๐ŸŽ“ A1Learner
๐Ÿ”
0 of 34 lessons completed

Go Mutex and WaitGroups

A mutex is a lock that lets one goroutine use shared data at a time. A WaitGroup joins a set of goroutines before the shared result is read.

A mutex counter reaches 300

A mutex is a lock that lets one goroutine use shared data at a time. Lock takes the lock and Unlock gives it back.

package main

import (
	"fmt"
	"sync"
)

func addHundred(mu *sync.Mutex, wg *sync.WaitGroup, count *int) {
	defer wg.Done()
	for i := 0; i < 100; i++ {
		mu.Lock()
		*count = *count + 1
		mu.Unlock()
	}
}

func main() {
	var mu sync.Mutex
	var wg sync.WaitGroup
	count := 0
	for worker := 0; worker < 3; worker++ {
		wg.Add(1)
		go addHundred(&mu, &wg, &count)
	}
	wg.Wait()
	fmt.Println("Count:", count)
}

Each worker writes its own slot

Each worker writes only the slot with its own index. Wait holds main back until each worker has called Done, so main reads finished slots.

package main

import (
	"fmt"
	"sync"
)

func squareInto(id int, results []int, wg *sync.WaitGroup) {
	defer wg.Done()
	results[id] = (id + 1) * (id + 1)
}

func main() {
	results := make([]int, 3)
	var wg sync.WaitGroup
	for id := 0; id < 3; id++ {
		wg.Add(1)
		go squareInto(id, results, &wg)
	}
	wg.Wait()
	fmt.Println(results)
}

Readers share the read lock

An RWMutex is a mutex with a read lock and a write lock. Many readers may hold the read lock together, and one writer holds the write lock alone. The writer finishes first, so every reader sees the final value.

package main

import (
	"fmt"
	"sync"
)

func writeScore(mu *sync.RWMutex, wg *sync.WaitGroup, score *int) {
	defer wg.Done()
	mu.Lock()
	*score = 42
	mu.Unlock()
}

func readScore(mu *sync.RWMutex, wg *sync.WaitGroup, score *int, results []int, id int) {
	defer wg.Done()
	mu.RLock()
	results[id] = *score
	mu.RUnlock()
}

func main() {
	var mu sync.RWMutex
	var wg sync.WaitGroup
	score := 0
	wg.Add(1)
	go writeScore(&mu, &wg, &score)
	wg.Wait()
	results := make([]int, 3)
	for id := 0; id < 3; id++ {
		wg.Add(1)
		go readScore(&mu, &wg, &score, results, id)
	}
	wg.Wait()
	fmt.Println(results)
}

Example

package main

import (
	"fmt"
	"sync"
)

func addPoints(mu *sync.Mutex, wg *sync.WaitGroup, score *int, points int) {
	defer wg.Done()
	mu.Lock()
	*score += points
	mu.Unlock()
}

func main() {
	var mu sync.Mutex
	var wg sync.WaitGroup
	score := 0
	amounts := []int{10, 20, 30}
	for _, amount := range amounts {
		wg.Add(1)
		go addPoints(&mu, &wg, &score, amount)
	}
	wg.Wait()
	fmt.Println("Score:", score)
}

Key points

  • A mutex is a lock that guards shared data.
  • Use Lock to take the lock before touching shared data.
  • Use Unlock to give the lock back after the change.
  • Only one goroutine holds a mutex at a time.
  • An RWMutex allows many readers at once or one writer alone.
  • Wait returns after the last worker calls Done.

In this example, three goroutines each add one amount to the shared score. Each goroutine holds the lock while it adds, so no two changes overlap. Main waits for all three goroutines and then prints the score. The amounts 10, 20, and 30 add up to 60 in any order.

Note

Programs pass a mutex by pointer so all goroutines use the same lock.

Try it Yourself

Edit the code, then press Run.

Running is simulated ยท Go Playground

Exercise

Why does the mutex counter reach 300 every time?