DMarlon Studio

Lesson 15 · 15 of 20

Mutex and shared state · Go

Protect data shared by several goroutines in Go with sync.Mutex: 55 buyers, 40 tickets and not one ticket sold twice.

Go · Mutex and shared state in DMarlon Studio: the example's code and its output in the terminal

When several goroutines read and write the same variable at once you have a data race. In Buy, checking whether tickets are left and taking one are two steps: if two buyers check at the same moment and both see 1, both buy it and a ticket that does not exist gets sold.

`sync.Mutex` (for *mutual exclusion*) prevents that. b.mu.Lock() lets a single goroutine through; the rest wait on that line until the first one calls Unlock. What lies between the two calls, the critical section, runs one goroutine at a time, so the check and the update happen together.

defer b.mu.Unlock() right after Lock is the usual style: the lock is released when the function returns, however it returns. The mutex lives inside the struct, next to the data it protects, and the comment says so. The methods use a pointer receiver: copying a Mutex breaks it (and go vet warns about it).

In main, 55 goroutines buy at once and wg.Wait() waits for all of them. Reading office.sold afterwards without the lock is fine, because Wait guarantees they have all finished. The result does not depend on the order: 40 are always sold, 15 buyers are turned away and none are left.

To catch races, run go run -race main.go: it reports two goroutines touching the same thing unprotected. If almost everything is reads there is sync.RWMutex; for a plain counter, sync/atomic.

Code · main.go

package mainimport (	"fmt"	"sync")type BoxOffice struct { // shared by many goroutines: mu protects the fields below	mu      sync.Mutex	tickets map[string]int	sold    int}func (b *BoxOffice) Buy(show string) {	b.mu.Lock()              // only one goroutine at a time gets past Lock	defer b.mu.Unlock()      // released when Buy returns	if b.tickets[show] > 0 { // check and update together: no ticket is sold twice		b.tickets[show]--		b.sold++	}}func main() {	office := &BoxOffice{tickets: map[string]int{"jazz night": 40}}	var wg sync.WaitGroup	for i := 0; i < 55; i++ { // 55 buyers arrive at the same time		wg.Add(1)		go func() { defer wg.Done(); office.Buy("jazz night") }()	}	wg.Wait()	fmt.Println("buyers: 55 | sold:", office.sold, "| turned away:", 55-office.sold)	fmt.Println("tickets left:", office.tickets["jazz night"])}

Output · go run main.go

buyers: 55 | sold: 40 | turned away: 15
tickets left: 0

Key points

  • Two goroutines writing the same thing unprotected are a race.
  • Lock lets one goroutine through until Unlock.
  • Write defer mu.Unlock() right after Lock().
  • Keep the mutex next to the data it protects; use a pointer receiver.
  • go run -race finds data races.

Exercise

Add a second show to the box office, "rock night", with 10 tickets, and make even-numbered buyers ask for one and odd-numbered buyers for the other. Print what is left of each at the end.