Kodewick
Start learning →
Lesson 22 of 24Contents

WaitGroup and Mutex

Lesson 22 of 24 · Part 5 · Concurrency · about 15 minutes

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.

prog.go
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}
Output
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

CodeMeaning
var wg sync.WaitGroupA 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.

prog.go
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}
Output
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:

Timegoroutine Agoroutine Bcount
1reads 55
2reads 55
3writes 66
4writes 66

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.

prog.go
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}
Output
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:

prog.go
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}
Output
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.

Guess first

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}

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:

prog.go
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}
Output
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.

Exercise 1

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.

It should print
[90 225 72]
[36 54]
exercise1.go
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}
Output
Press Run or Ctrl+Enter
Exercise 2

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.

It should print
100
115
exercise2.go
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}
Output
Press Run or Ctrl+Enter
Exercise 3

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.

It should print
true 70
false 70
true 0
exercise3.go
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}
Output
Press Run or Ctrl+Enter

What you learned

  • sync.WaitGroup is a counter: wg.Add(1) before go, defer wg.Done() inside the goroutine, and wg.Wait() waits for 0.
  • Call Add in main, before the go; 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.Mutex lets one goroutine at a time into a piece of code: mu.Lock() … mu.Unlock().
  • Right after taking the lock, defer mu.Unlock(), and an early return can't forget to unlock.
Finished this lesson?Mark it. Progress is saved in this browser, and you can undo it.