用简单的例子来理解 sync.Mutex
和 sync.RWMutex
盖一间银行
假设有一间银行,可以存款和查询余额。
package main
import (
"fmt"
)
type Bank struct {
balance int
}
func (b *Bank) Deposit(amount int) {
b.balance += amount
}
func (b *Bank) Balance() int {
return b.balance
}
func main() {
b := &Bank{}
b.Deposit(1000)
b.Deposit(1000)
b.Deposit(1000)
fmt.Println(b.Balance())
}
$ go run main.go
3000
执行之后结果是 3000 没问题,1000+1000+1000=3000
同时存款
银行不太可能让人一个一个排队存款,也需要支持同时存款,当今天存款的动作是并行的,会发生什么事呢?
这次用 sync.WaitGroup 去等待所有 goroutine 执行完毕,之后再打印出余额。
func main() {
var wg sync.WaitGroup
b := &Bank{}
wg.Add(3)
go func() {
b.Deposit(1000)
wg.Done()
}()
go func() {
b.Deposit(1000)
wg.Done()
}()
go func() {
b.Deposit(1000)
wg.Done()
}()
wg.Wait()
fmt.Println(b.Balance())
}
$ go run main.go
3000
还是 3000 没问题,那我们同时存款 1000 次的时候会发生什么事呢?
func main() {
var wg sync.WaitGroup
b := &Bank{}
n := 1000
wg.Add(n)
for i := 1; i <= n; i++ {
go func() {
b.Deposit(1000)
wg.Done()
}()
}
wg.Wait()
fmt.Println(b.Balance())
}
$ go run main.go
958000
诶奇怪,正常来说 1000 * 1000 = 1000000 吗?怎么数字不正确!
我们这次多带一个参数 -race
跑看看
-race
参数是 go 的 Race Detector,内建整合工具,可以轻松检查出是否有 race condition
$ go run -race main.go
==================
WARNING: DATA RACE
Read at 0x00c00012c088 by goroutine 8:
main.(*Bank).Deposit()
.../main.go:13 +0x3c
main.main.func1()
.../main.go:28 +0x37
Previous write at 0x00c00012c088 by goroutine 7:
main.(*Bank).Deposit()
.../main.go:13 +0x4e
main.main.func1()
.../main.go:28 +0x37
Goroutine 8 (running) created at:
main.main()
.../main.go:27 +0x97
Goroutine 7 (finished) created at:
main.main()
.../main.go:27 +0x97
==================
1000000
Found 1 data race(s)
exit status 66
发现原来有 race condition, 因为同时去对 Bank.balance 去做存取的动作,数量少的时候可能没问题,当量大的时候就可能出错。
sync.Mutex
为了防止这种状况发生,就可以用互斥锁 sync.Mutex 来处理这个问题,同时间只有一个 goroutine 能存取该变数。
这次我们在 Deposit()
存款前先 Lock()
,存款后再 Unlock()
。
type Bank struct {
balance int
mux sync.Mutex
}
func (b *Bank) Deposit(amount int) {
b.mux.Lock()
b.balance += amount
b.mux.Unlock()
}
func (b *Bank) Balance() int {
return b.balance
}
$ go run -race main.go
1000000
这次结果正确了,而且也没跳出 race condition 的警告。
同时存款和查询
想当然会有多人一起存款,就会有多人一起查询余额。
多加一组查询 1000 次的 goroutine 再执行看看。
func main() {
var wg sync.WaitGroup
b := &Bank{}
n := 1000
wg.Add(n)
for i := 1; i <= n; i++ {
go func() {
b.Deposit(1000)
wg.Done()
}()
}
wg.Add(n)
for i := 1; i <= n; i++ {
go func() {
_ = b.Balance()
wg.Done()
}()
}
wg.Wait()
fmt.Println(b.Balance())
}
$ go run -race main.go
==================
WARNING: DATA RACE
Read at 0x00c00012a080 by goroutine 56:
main.(*Bank).Balance()
.../main.go:20 +0x3c
main.main.func2()
.../main.go:39 +0x37
Previous write at 0x00c00012a080 by goroutine 53:
main.(*Bank).Deposit()
.../main.go:15 +0x5d
main.main.func1()
.../main.go:32 +0x3e
Goroutine 56 (running) created at:
main.main()
.../main.go:38 +0x184
Goroutine 53 (finished) created at:
main.main()
.../main.go:31 +0xa4
==================
1000000
Found 1 data race(s)
exit status 66
不意外,因为同时对 balance 去做读写,当然跳出 race condition 的警告。
我们一样在 Balance()
加上 Lock()
和 Unlock()
后执行。
type Bank struct {
balance int
mux sync.Mutex
}
func (b *Bank) Deposit(amount int) {
b.mux.Lock()
b.balance += amount
b.mux.Unlock()
}
func (b *Bank) Balance() (balance int) {
b.mux.Lock()
balance = b.balance
b.mux.Unlock()
return
}
$ go run -race main.go
1000000
结果成功了,也没有 race 的警告了。
读写互相阻塞
目前这边看起来都还不错,但以现在的情况来说,只要有人读,或只要有人写,就会被 block。
假如银行存款和查询各要上花一秒
package main
import (
"log"
"sync"
"time"
)
type Bank struct {
balance int
mux sync.Mutex
}
func (b *Bank) Deposit(amount int) {
b.mux.Lock()
time.Sleep(time.Second) // spend 1 second
b.balance += amount
b.mux.Unlock()
}
func (b *Bank) Balance() (balance int) {
b.mux.Lock()
time.Sleep(time.Second) // spend 1 second
balance = b.balance
b.mux.Unlock()
return
}
func main() {
var wg sync.WaitGroup
b := &Bank{}
n := 5
wg.Add(n)
for i := 1; i <= n; i++ {
go func() {
b.Deposit(1000)
log.Printf("Write: deposit amonut: %v", 1000)
wg.Done()
}()
}
wg.Add(n)
for i := 1; i <= n; i++ {
go func() {
log.Printf("Read: balance: %v", b.Balance())
wg.Done()
}()
}
wg.Wait()
}
$ go run -race main.go
2022/02/18 15:12:49 Write: deposit amonut: 1000
2022/02/18 15:12:50 Write: deposit amonut: 1000
2022/02/18 15:12:51 Write: deposit amonut: 1000
2022/02/18 15:12:52 Write: deposit amonut: 1000
2022/02/18 15:12:53 Write: deposit amonut: 1000
2022/02/18 15:12:54 Read: balance: 5000
2022/02/18 15:12:55 Read: balance: 5000
2022/02/18 15:12:56 Read: balance: 5000
2022/02/18 15:12:57 Read: balance: 5000
2022/02/18 15:12:58 Read: balance: 5000
就会发现,每隔一秒才能处理一个 action,以各五次读写来说,总共就要花上 10 秒,但对读来说,应该可以疯狂读,每次读都会是安全的, 值也都会是一样,除非当下有写的动作,它不应该被其他读的动作 block。
sync.RWMutex
sync.RWMutex
是一个读写锁(multiple readers, single writer lock),多读单写,可以允许多个读并发,单个写。
把 sync.Mutex
换成 sync.RWMutex
type Bank struct {
balance int
mux sync.RWMutex // read write lock
}
func (b *Bank) Deposit(amount int) {
b.mux.Lock() // write lock
time.Sleep(time.Second)
b.balance += amount
b.mux.Unlock() // wirte unlock
}
func (b *Bank) Balance() (balance int) {
b.mux.RLock() // read lock
time.Sleep(time.Second)
balance = b.balance
b.mux.RUnlock() // read unlock
return
}
$ go run -race main.go
2022/02/18 15:16:29 Write: deposit amonut: 1000
2022/02/18 15:16:30 Read: balance: 1000
2022/02/18 15:16:30 Read: balance: 1000
2022/02/18 15:16:30 Read: balance: 1000
2022/02/18 15:16:30 Read: balance: 1000
2022/02/18 15:16:30 Read: balance: 1000
2022/02/18 15:16:31 Write: deposit amonut: 1000
2022/02/18 15:16:32 Write: deposit amonut: 1000
2022/02/18 15:16:33 Write: deposit amonut: 1000
2022/02/18 15:16:34 Write: deposit amonut: 1000
执行之后会发现,本来要花 10 秒,已经缩短成 5 秒了,只要当下是读的时候,都会同时进行,并不会互相影响,写的时候就会 block 读和写,只有一个写会发生。
总结
- 在写 goroutine 的时候,需要考虑 race condition,在执行或测试上可以加上
-race
去检查,以免结果与预期不符 - 遇到 race condition 的时候可以考虑用
sync.Mutex
来解决,有读写阻塞的时候可以用sync.RWMutex
syncRWMutex
可以有同时允许多个RLock
和RUnlock
但只能有一个Lock
和Unlock
go Benchmark
Benchmark: sync.RWMutex vs atomic.Value
/*
Benchmark_RWMutex-4 100000000 18.1 ns/op
Benchmark_Atomic-4 200000000 8.01 ns/op
Benchmark_RWMutex_parallel-4 30000000 46.7 ns/op
Benchmark_Atomic_parallel-4 1000000000 2.61 ns/op
*/
package main
import (
"sync"
"sync/atomic"
"testing"
)
func Benchmark_RWMutex(b *testing.B) {
var lock sync.RWMutex
m := map[int]int{1: 2}
for i := 0; i < b.N; i++ {
lock.RLock()
_ = m[1]
lock.RUnlock()
}
}
func Benchmark_Atomic(b *testing.B) {
var ptr atomic.Value
ptr.Store(map[int]int{1: 2})
for i := 0; i < b.N; i++ {
m := ptr.Load().(map[int]int)
_ = m[1]
}
}
func Benchmark_RWMutex_parallel(b *testing.B) {
var lock sync.RWMutex
m := map[int]int{1: 2}
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
lock.RLock()
_ = m[1]
lock.RUnlock()
}
})
}
func Benchmark_Atomic_parallel(b *testing.B) {
var ptr atomic.Value
ptr.Store(map[int]int{1: 2})
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
m := ptr.Load().(map[int]int)
_ = m[1]
}
})
}
$ go mod init benchmark
$ go test -cpu 1,2,4,8,36 -bench .
goos: windows
goarch: amd64
pkg: benchmark
cpu: Intel(R) Core(TM) i9-10980XE CPU @ 3.00GHz
Benchmark_RWMutex 84223528 14.31 ns/op
Benchmark_RWMutex-2 82506548 14.48 ns/op
Benchmark_RWMutex-4 80424642 14.46 ns/op
Benchmark_RWMutex-8 82474226 14.82 ns/op
Benchmark_RWMutex-36 72245634 14.67 ns/op
Benchmark_Atomic 298416230 4.013 ns/op
Benchmark_Atomic-2 294261531 4.014 ns/op
Benchmark_Atomic-4 298208067 4.050 ns/op
Benchmark_Atomic-8 278849458 4.110 ns/op
Benchmark_Atomic-36 289697505 4.060 ns/op
Benchmark_RWMutex_parallel 82412020 14.31 ns/op
Benchmark_RWMutex_parallel-2 39356391 30.50 ns/op
Benchmark_RWMutex_parallel-4 37746673 31.62 ns/op
Benchmark_RWMutex_parallel-8 38003095 31.19 ns/op
Benchmark_RWMutex_parallel-36 41561199 28.36 ns/op
Benchmark_Atomic_parallel 297627019 4.096 ns/op
Benchmark_Atomic_parallel-2 566516064 2.016 ns/op
Benchmark_Atomic_parallel-4 1000000000 1.029 ns/op
Benchmark_Atomic_parallel-8 1000000000 0.5073 ns/op
Benchmark_Atomic_parallel-36 100000000 14.80 ns/op
PASS
ok benchmark 29.463s
Go 簡單例子來理解 sync.Mutex 和 sync.RWMutex
sync.RWMutex
atomutex_test.go