sync包还是channel
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| var cs = 0 var mu sync.Mutex var c = make(chan struct{}, 1)
func criticalSectionSyncByMutex() { mu.Lock() cs++ mu.Unlock() }
func criticalSectionSyncByChan() { c <- struct{}{} cs++ <-c }
func BenchmarkCriticalSectionSyncByMutex(b *testing.B) { for n := 0; n < b.N; n++ { criticalSectionSyncByMutex() } }
func BenchmarkCriticalSectionSyncByChan(b *testing.B) { for n := 0; n < b.N; n++ { criticalSectionSyncByChan() } }
|
使用sync包的注意事项
在sync
包源文件中,我们看到以下注释:
为什么在Mutex
等sync
包中定义的结构类型首次使用后不应该对其进行复制操作呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
| type foo struct { n int sync.Mutex }
func main() { f := foo{n: 17}
go func(f foo) { for { log.Println("g2: try to lock foo...") f.Lock() log.Println("g2: lock foo ok") time.Sleep(3 * time.Second) f.Unlock() log.Println("g2: unlock foo ok") } }(f)
f.Lock() log.Println("g1: lock foo ok")
go func(f foo) { for { log.Println("g3: try to lock foo...") f.Lock() log.Println("g3: lock foo ok") time.Sleep(5 * time.Second) f.Unlock() log.Println("g3: unlock foo ok") } }(f)
time.Sleep(1000 * time.Second) f.Unlock() log.Println("g1: unlock foo ok") }
|
结果显示:g3阻塞在加锁操作上,g2则正常运行。
原因分析:
Go标准库的sync.Mutex
定义如下:
1 2 3 4
| type Mutex struct { state int32 sema uint32 }
|
对Mutex
实例的复制就是对两个整型字段的复制。
在初始状态下,Mutex
实例处于Unlocked
状态(state和sema均为0)。
g2复制了处于初始状态的Mutex
实例,副本的state和sema均为0,与g2自定义一个新的Mutex
无异,因此可以按预期正常运行;后续主程序调用了Lock
方法,Mutex
实例变为Locked
状态(state值改变),而后面g3创建时正好复制了处于Locked
状态的Mutex
实例,因此g3再对其实例副本调用Lock
方法将会阻塞。
结论:使用sync包中类型时,推荐通过闭包方式或传递类型实例(或包裹该类型的类型实例)的地址或指针的方法进行。
互斥锁还是读写锁
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93
| var cs1 = 0 var mu1 sync.Mutex var cs2 = 0 var mu2 sync.RWMutex
func BenchmarkReadSyncByMutex(b *testing.B) { b.RunParallel(func(pb *testing.PB) { for pb.Next() { mu1.Lock() _ = cs1 mu1.Unlock() } }) }
func BenchmarkReadSyncByRWMutex(b *testing.B) { b.RunParallel(func(pb *testing.PB) { for pb.Next() { mu2.RLock() _ = cs2 mu2.RUnlock() } }) }
func BenchmarkWriteSyncByRWMutex(b *testing.B) { b.RunParallel(func(pb *testing.PB) { for pb.Next() { mu2.Lock() cs2++ mu2.Unlock() } }) }
|
结论:
- 在并发量较小的情况下,互斥锁性能更好;随着并发量增大,互斥锁竞争激烈,导致加锁和解锁性能下降;
- 读写锁的读锁性能并未随着并发量的增大而发生较大变化,性能始终恒定在29ns左右;
- 在并发量较大的情况下,读写锁的写锁性能比互斥锁、读写锁的读锁性能都差,并且随着并发量的增大,其写锁性能有持续下降的趋势