WaitGroup and Mutex
In lesson 19 a channel let main wait for one goroutine. When there are many goroutines to wait for, and you don't need values back from them, a WaitGroup from the sync package is simpler: it's a counter of how many are still working.
1package main2 3import (4 "fmt"5 "sync"6)7 8func main() {9 var wg sync.WaitGroup10 sizes := []int{1200, 800, 2000}11 thumbs := make([]int, len(sizes))12 for i, s := range sizes {13 wg.Add(1)14 go func() {15 defer wg.Done()16 thumbs[i] = s / 1017 }()18 }19 wg.Wait()20 fmt.Printf("Thumbnail sizes: %v\n", thumbs)21}
Thumbnail sizes: [120 80 200]
Three images (sizes in KB) each go to their own goroutine, which makes a thumbnail a tenth of the size. wg.Wait() waits until all three are done before main moves on, so what you see is always the finished result.
Add, Done, Wait
| Code | Meaning |
|---|---|
var wg sync.WaitGroup | A counter, ready to use as its zero value, starting at 0. |
wg.Add(1) | Add 1: one more goroutine to wait for. |
wg.Done() | Subtract 1: this goroutine has finished. |
wg.Wait() | Wait until the counter is back to 0, then go on. |
Write defer wg.Done() as the goroutine's first line: however the goroutine ends, Done runs (the defer of lesson 15).
Each goroutine writes its own slot of thumbs (thumbs[i]) and never touches anyone else's, so they can't get in each other's way. Press Step: each goroutine gets its own lane, and main sits at wg.Wait() while the other lanes finish one by one.
Pass a WaitGroup by pointer
When the work is in a function of its own, pass &wg, with the parameter type *sync.WaitGroup. Pass wg itself and the function gets a copy (lesson 11): its Done lowers the copy's counter, and the Wait in main waits forever.
1package main2 3import (4 "fmt"5 "sync"6)7 8func pack(i int, items int, boxes []int, wg *sync.WaitGroup) {9 defer wg.Done()10 boxes[i] = (items + 9) / 1011}12 13func main() {14 orders := []int{25, 10, 3, 41}15 boxes := make([]int, len(orders))16 var wg sync.WaitGroup17 wg.Add(len(orders))18 for i, n := range orders {19 go pack(i, n, boxes, &wg)20 }21 wg.Wait()22 fmt.Printf("Boxes for each order: %v\n", boxes)23}
Boxes for each order: [3 1 1 5]
A box holds 10 items, and (items + 9) / 10 rounds up. When you know how many goroutines there will be, one wg.Add(len(orders)) is enough.
Add before go
Call wg.Add in main, before the go, not inside the goroutine:
1for i, s := range sizes {2 go func() {3 wg.Add(1) // wrong: too late4 defer wg.Done()5 thumbs[i] = s / 106 }()7}8wg.Wait()
Nobody knows exactly when a new goroutine starts running. If main reaches wg.Wait() first, nobody has called Add yet, the counter is 0, and Wait returns at once, before the work is done. Add in main, then go, and that can't happen.
Many goroutines, one variable
Writing separate slots is fine. But what if several goroutines change the same variable? Say 1000 goroutines each add 1 to a counter:
1count := 02var wg sync.WaitGroup3for range 1000 {4 wg.Add(1)5 go func() {6 defer wg.Done()7 count++8 }()9}10wg.Wait()11fmt.Println(count)
count++ looks like one step, but it's three: read count, add 1, write it back. On a computer with several CPU cores, goroutines really do run at the same time, and this can happen:
| Time | goroutine A | goroutine B | count |
|---|---|---|---|
| 1 | reads 5 | 5 | |
| 2 | reads 5 | 5 | |
| 3 | writes 6 | 6 | |
| 4 | writes 6 | 6 |
Two additions, but the count only went up by 1. Run this a few times on a real machine and you may see 1000, or 987, or 962: a different number each time. This is called a data race.
This page runs one goroutine at a time and only switches when it stops to wait (on a channel, Wait, Lock or Sleep), so here the program always shows 1000. Don't be fooled: on a real machine it's wrong.
Tip: On your own computer, run it with go run -race prog.go. Go finds the data races that actually happen during that run and tells you the line.
sync.Mutex: one at a time
A Mutex (mutual exclusion lock) guards a piece of code: mu.Lock() takes the lock and mu.Unlock() gives it back. While someone else has the lock, Lock() waits until it's given back. So nobody can slip in between the read, the add and the write.
1package main2 3import (4 "fmt"5 "sync"6)7 8func main() {9 count := 010 var mu sync.Mutex11 var wg sync.WaitGroup12 for range 1000 {13 wg.Add(1)14 go func() {15 defer wg.Done()16 mu.Lock()17 count++18 mu.Unlock()19 }()20 }21 wg.Wait()22 fmt.Println(count)23}
1000
On any computer, however many times you run it: 1000.
The next program makes the slow part bigger. A deposit reads the balance, waits a moment (time.Sleep, which doesn't really wait here), then writes the new balance. Three deposits go into one account at once:
1package main2 3import (4 "fmt"5 "sync"6 "time"7)8 9type Account struct {10 mu sync.Mutex11 balance int12}13 14func (a *Account) Deposit(amount int) {15 a.mu.Lock()16 b := a.balance17 time.Sleep(10 * time.Millisecond)18 a.balance = b + amount19 a.mu.Unlock()20}21 22func main() {23 acc := &Account{}24 var wg sync.WaitGroup25 for _, amount := range []int{100, 50, 30} {26 wg.Add(1)27 go func() {28 defer wg.Done()29 acc.Deposit(amount)30 }()31 }32 wg.Wait()33 fmt.Printf("Balance: %d\n", acc.balance)34}
Balance: 180
Press Step: the lane holding the lock is in Sleep, and the other two wait at a.mu.Lock() for it to unlock. Then try deleting the Lock and Unlock lines and run it again: while one goroutine sleeps, the others come in and read the old balance, and the final balance isn't 180. This mistake you can see here too.
The lock lives in the struct and the method has a pointer receiver (*Account): every goroutine uses the same account and the same lock.
What does this program show on a real machine?
1package main2 3import (4 "fmt"5 "sync"6)7 8func main() {9 total := 010 var mu sync.Mutex11 var wg sync.WaitGroup12 for range 5 {13 wg.Add(1)14 go func() {15 defer wg.Done()16 for range 10 {17 mu.Lock()18 total += 219 mu.Unlock()20 }21 }()22 }23 wg.Wait()24 fmt.Println(total)25}
5 goroutines each add 2 ten times: 100 in all. Every total += 2 happens inside the lock, so nobody can slip in, and it's 100 wherever it runs. Without the lock it could be less than 100 on a real machine.
defer mu.Unlock()
When a function has several returns, it's easy for one path to forget to unlock. A lock that's never given back makes the next Lock() wait forever. The usual habit is defer mu.Unlock() right after taking the lock:
1package main2 3import (4 "fmt"5 "sync"6)7 8type Store struct {9 mu sync.Mutex10 stock map[string]int11 failed int12}13 14func (s *Store) Order(item string, n int) {15 s.mu.Lock()16 defer s.mu.Unlock()17 if s.stock[item] < n {18 s.failed++19 return20 }21 s.stock[item] -= n22}23 24func main() {25 s := &Store{stock: map[string]int{"apples": 20, "milk": 8}}26 var wg sync.WaitGroup27 orders := []int{5, 3, 50, 7}28 for _, n := range orders {29 wg.Add(1)30 go func() {31 defer wg.Done()32 s.Order("apples", n)33 }()34 }35 wg.Add(1)36 go func() {37 defer wg.Done()38 s.Order("milk", 2)39 }()40 wg.Wait()41 fmt.Printf("Stock: %v\n", s.stock)42 fmt.Printf("Failed: %d\n", s.failed)43}
Stock: map[apples:5 milk:6] Failed: 1
When there isn't enough stock, the early return still unlocks, thanks to defer. The map is changed by several goroutines too, so it's changed inside the lock as well. The order for 50 apples is too big wherever it lands in line, so the result doesn't depend on who goes first.
Practice
Write the code, then press Check. A hint costs nothing; the answer is there when you need it.
Wait for every quote
✓A goroutine works out each shop's quote (the price minus 10%), but main shows the results without waiting for them. Use a sync.WaitGroup so the function waits for every goroutine. The program works out two sets of prices.
[90 225 72] [36 54]
1package main2 3import "fmt"4 5func quotes(prices []int) []int {6 result := make([]int, len(prices))7 for i, p := range prices {8 go func() {9 result[i] = p * 9 / 1010 }()11 }12 return result13}14 15func main() {16 fmt.Println(quotes([]int{100, 250, 80}))17 fmt.Println(quotes([]int{40, 60}))18}
Tills that share one total
✓Each till is a goroutine that adds its payments, one by one, to one shared total. Each payment reads the total, waits a moment for the card machine (Sleep), then writes it back. Right now the total comes out wrong. Add a sync.Mutex so nobody can slip in between the read and the write. The program adds up two days.
100 115
1package main2 3import (4 "fmt"5 "sync"6 "time"7)8 9func addAll(tills [][]int) int {10 total := 011 var wg sync.WaitGroup12 for _, till := range tills {13 wg.Add(1)14 go func() {15 defer wg.Done()16 for _, amount := range till {17 t := total18 time.Sleep(time.Millisecond)19 total = t + amount20 }21 }()22 }23 wg.Wait()24 return total25}26 27func main() {28 fmt.Println(addAll([][]int{{10, 20}, {5}, {30, 15, 20}}))29 fmt.Println(addAll([][]int{{7, 8}, {100}}))30}
Fix it: an early return that keeps the lock
✓This program gets stuck: when the balance is too low, the withdrawal returns early and never gives the lock back. Unlock with defer instead, so every path unlocks.
true 70 false 70 true 0
1package main2 3import (4 "fmt"5 "sync"6)7 8type Wallet struct {9 mu sync.Mutex10 balance int11}12 13func (w *Wallet) Withdraw(n int) bool {14 w.mu.Lock()15 if w.balance < n {16 return false17 }18 w.balance -= n19 w.mu.Unlock()20 return true21}22 23func main() {24 w := &Wallet{balance: 100}25 for _, n := range []int{30, 500, 70} {26 ok := w.Withdraw(n)27 fmt.Println(ok, w.balance)28 }29}
What you learned
sync.WaitGroupis a counter:wg.Add(1)beforego,defer wg.Done()inside the goroutine, andwg.Wait()waits for 0.- Call
Addinmain, before thego; pass a WaitGroup to a function as a pointer,&wg. - Goroutines that each write their own slot (
results[i]) don't get in each other's way. - Goroutines changing one variable is a data race: on a real machine the result can differ from run to run; here only one goroutine runs at a time, so it usually doesn't show.
sync.Mutexlets one goroutine at a time into a piece of code:mu.Lock()…mu.Unlock().- Right after taking the lock,
defer mu.Unlock(), and an earlyreturncan't forget to unlock.