用简单的例子来理解 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 能存取改变该数。通过 sync.Mutex
实现线程安全
这次我们在 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())
}
不意外, 此时会再次出现竞争条件警告, 因为同时对 balance 去做读写操作会相互影响, 当然会跳出 race condition 警告
$ 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()
加上 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
假如银行每次存款和查询都需要 1 秒钟
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
就会发现, 每隔 1 秒才能处理一个操作, 5 次存款和 5 次查询总共需要 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
去检查, 以免结果与预期不符 - 使用
sync.Mutex
可以解决竞争条件, 但读写操作会互相阻塞 - 使用
sync.RWMutex
可以提升性能, 允许多个读操作同时进行, 但写操作仍会阻塞所有读写
go Benchmark
atomutex_test.go 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