用简单的例子来理解 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