Skip to main content

互斥体 Mutexes

Mutextes in Go

Mutexes allow us to lock access to data. This ensures that we can control which goroutines can access certain data at which time.

Go's standard library provides a built-in implementation of a mutex with the sync.Mutex type and its two methods:

  • .Lock()
  • .Unlock()

We can protect a block of code by surrounding it with a call to Lock and Unlock as shown on the protected() method below.

It's good practice to structure the protected code winthin a function so that defer can be used to ensure that we nerver forget to unlock the mutex.

func protected() {
mux.Lock()
defer mux.Unlock()
// the rest of the function is protected
// any other calls to `mux.Lock()` will block
}

Mutexes are powerful. Like most powerful things, they can also cause many bugs if used carelessly.

Maps are not thread-safe

Maps are not safe for concurrent use! If you have multiple goroutines accessing the same map, and at least one of them is writing to the map, you must lock your maps with a mutex.

Example

type safeCounter struct {
counts map[string]int
mux *sync.Mutex
}

func (sc safeCounter) inc(key string) {
// sc.slowIncrement(key) // 这里线程对 map 的处理不安全,需要使用 mutex

sc.mux.Lock()
defer sc.mux.Unlock()
sc.slowIncrement(key)
}

func (sc safeCounter) val(key string) int {
sc.mux.Lock()
defer sc.mux.Unlock()
return sc.counts[key] // 对 map 读取操作也不是安全的
}

func (sc safeCounter) slowIncrement(key string) {
tempCoutner := sc.counts[key]
time.Sleep(time.Microsecond)
tempCoutner++
sc.counts[key] = tempCounter
}

type emailTest struct {
email string
count int
}

func test(sc safeCounter, emailTests []emailTest) {
emails := make(map[string]struct{})

var wg sync.WaitGroup
for _, emailT := range emailTests {
emails[emailT.email] = struct{}{}
for i := 0; i< emailT.count; i++ {
wg.Add(1)
go func(emailT emailTest) {
sc.inc(emailT.email)
wg.Done()
}(emailT)
}
}
wg.Wait()

emailsSorted := make([]string, 0, len(emails))
for email := range emails {
emailsSorted = append(emailsSorted, email)
}
sort.Strings(emailsSorted)

for _, email := range emailsSorted {
fmt.Printf("Email: %s has %d emails\n", email, sc.val(email))
}
fmt.Println("====================================")
}

func main() {
sc := safeCounter{
counts: make(map[string]int),
mux: &sync.Mutex{},
}

test(sc, []emailTest{
{
email: "[email protected]",
count: 23,
},
{
email: "[email protected]",
count: 29,
},
{
email: "[email protected]",
count: 31,
},
{
email: "[email protected]",
count: 67,
},
})
}

Why is it called a "mutex"?

Mutex is short for mutual exclusion, and the conventional name ofr the data structure that provides it is "mutex", often abbreviated to "mux".

It's called "mutual exclusion" because a mutex excludes different threads (or goroutines) from accessing the same data at the same time.

Mutex review

The principle problem that mutexes help us avoid is the concurrent read/write problem. This problem arises when one thread is writing to a variable while another thread is reading from that same variable at the same time.

When this happens, a Go program will panic because the reader could be reading bad data while it's being mutated in places.

RW Mutetx

The standard library also exposes a sync.RWMutex.

In addition to these methods:

  • .Lock()
  • .Unlock()

The sync.RWMutex also has these methods:

  • .RLock()
  • .RUnlock()

The sync.RWMutex can help with performance if we have a read-intensive process. Many goroutines can safely read from the map at the same time (multiple Rlock() calls can happen simultaneously). However, only one goroutine can hold a Lock() and all RLock()'s will also be excluded.

Previous example with RWMutex

func (sc safeCounter) val(key string) int {
sc.mux.RLock()
defer sc.mux.RUnlock()
return sc.counts[key] // 对 map 读取操作也不是安全的
}

Rread/Write Mutex review

Maps are safe for concurrent read access, just not concurrent read/write or write/write access. A read/write mutex allows all the readers to access the map at the same time, but a writer will still lock out all other readers and writers.