不会飞的章鱼

熟能生巧,勤能补拙;念念不忘,必有回响。

Go语言精进——了解sync包的正确用法

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()
}
}

/*
BenchmarkCriticalSectionSyncByMutex-8 76766581 15.41 ns/op
BenchmarkCriticalSectionSyncByChan-8 32243965 37.59 ns/op
*/

使用sync包的注意事项

sync包源文件中,我们看到以下注释:

1
2
3
4
5
// A Mutex must not be copied after first use.

// A RWMutex must not be copied after first use.

// A Cond must not be copied after first use.

为什么在Mutexsync包中定义的结构类型首次使用后不应该对其进行复制操作呢?

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) { // 创建协程2
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")

// 在mutex首次使用后复制其值
go func(f foo) { // 创建协程3
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")
}

/*
2022/10/11 17:21:50 g1: lock foo ok
2022/10/11 17:21:50 g3: try to lock foo...
2022/10/11 17:21:50 g2: try to lock foo...
2022/10/11 17:21:50 g2: lock foo ok
2022/10/11 17:21:53 g2: unlock foo ok
2022/10/11 17:21:53 g2: try to lock foo...
2022/10/11 17:21:53 g2: lock foo ok
2022/10/11 17:21:56 g2: unlock foo ok
2022/10/11 17:21:56 g2: try to lock foo...
2022/10/11 17:21:56 g2: lock foo ok
2022/10/11 17:21:59 g2: unlock foo ok
2022/10/11 17:21:59 g2: try to lock foo...
2022/10/11 17:21:59 g2: lock foo ok
2022/10/11 17:22:02 g2: unlock foo ok
2022/10/11 17:22:02 g2: try to lock foo...
2022/10/11 17:22:02 g2: lock foo ok
2022/10/11 17:22:05 g2: 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()
}
})
}

/*
$ go test -bench . go-sync-package-3_test.go -cpu 2
BenchmarkReadSyncByMutex-2 66311919 16.16 ns/op
BenchmarkReadSyncByRWMutex-2 100000000 28.93 ns/op
BenchmarkWriteSyncByRWMutex-2 37036560 34.01 ns/op
PASS
ok command-line-arguments 5.290s

$ go test -bench . go-sync-package-3_test.go -cpu 8
goos: linux
goarch: amd64
cpu: 11th Gen Intel(R) Core(TM) i5-1135G7 @ 2.40GHz
BenchmarkReadSyncByMutex-8 20932657 56.80 ns/op
BenchmarkReadSyncByRWMutex-8 43208829 27.58 ns/op
BenchmarkWriteSyncByRWMutex-8 19324240 63.63 ns/op
PASS
ok command-line-arguments 3.765s

$ go test -bench . go-sync-package-3_test.go -cpu 16
goos: linux
goarch: amd64
cpu: 11th Gen Intel(R) Core(TM) i5-1135G7 @ 2.40GHz
BenchmarkReadSyncByMutex-16 15072116 73.57 ns/op
BenchmarkReadSyncByRWMutex-16 40002030 29.30 ns/op
BenchmarkWriteSyncByRWMutex-16 16487802 77.27 ns/op
PASS
ok command-line-arguments 3.745s

$ go test -bench . go-sync-package-3_test.go -cpu 32
goos: linux
goarch: amd64
cpu: 11th Gen Intel(R) Core(TM) i5-1135G7 @ 2.40GHz
BenchmarkReadSyncByMutex-32 10650474 108.4 ns/op
BenchmarkReadSyncByRWMutex-32 40073268 29.98 ns/op
BenchmarkWriteSyncByRWMutex-32 11371809 102.0 ns/op
PASS
ok command-line-arguments 3.773s

$ go test -bench . go-sync-package-3_test.go -cpu 64
goos: linux
goarch: amd64
cpu: 11th Gen Intel(R) Core(TM) i5-1135G7 @ 2.40GHz
BenchmarkReadSyncByMutex-64 10868832 118.0 ns/op
BenchmarkReadSyncByRWMutex-64 39970500 29.89 ns/op
BenchmarkWriteSyncByRWMutex-64 7829715 139.1 ns/op
PASS
ok command-line-arguments 3.872s

$ go test -bench . go-sync-package-3_test.go -cpu 128
goos: linux
goarch: amd64
cpu: 11th Gen Intel(R) Core(TM) i5-1135G7 @ 2.40GHz
BenchmarkReadSyncByMutex-128 11568276 118.7 ns/op
BenchmarkReadSyncByRWMutex-128 42309298 27.27 ns/op
BenchmarkWriteSyncByRWMutex-128 7173530 160.3 ns/op
PASS
ok command-line-arguments 3.996s
*/

结论:

  • 在并发量较小的情况下,互斥锁性能更好;随着并发量增大,互斥锁竞争激烈,导致加锁和解锁性能下降;
  • 读写锁的读锁性能并未随着并发量的增大而发生较大变化,性能始终恒定在29ns左右;
  • 在并发量较大的情况下,读写锁的写锁性能比互斥锁、读写锁的读锁性能都差,并且随着并发量的增大,其写锁性能有持续下降的趋势
------ 本文结束------
如果本篇文章对你有帮助,可以给作者加个鸡腿~(*^__^*),感谢鼓励与支持!