原子操作 atomic包
# 6. 原子操作 atomic 包
面向 CSP 并发模型的 channel 原语和面向传统共享内存并发模型的 sync 包提供的原语,已经能够满足 Go 语言应用并发设计中 99.9% 的并发同步需求了。而剩余那 0.1% 的需求,可以使用 Go 标准库提供的 atomic 包来实现。
atomic 包是 Go 语言给用户提供的原子操作原语的相关接口。原子操作(atomic operations)是相对于普通指令操作而言的。
整型变量自增的语句示例:
var a int a++
1
2a++ 这行语句需要 3 条普通机器指令来完成变量 a 的自增:
- LOAD:将变量从内存加载到 CPU 寄存器;
- ADD:执行加法指令;
- STORE:将结果存储回原内存地址中。
这 3 条普通指令在执行过程中是可以被中断的。而原子操作的指令是不可中断的,它就好比一个事务,要么不执行,一旦执行就一次性全部执行完毕,中间不可分割。也正因为如此,原子操作也可以被用于共享数据的并发同步。
原子操作由底层硬件直接提供支持,是一种硬件实现的指令级的“事务”,因此相对于操作系统层面和 Go 运行时层面提供的同步技术而言,它更为原始。
atomic 包封装了 CPU 实现的部分原子操作指令,为用户层提供体验良好的原子操作函数,因此 atomic 包中提供的原语更接近硬件底层,也更为低级,它也常被用于实现更为高级的并发同步技术,比如 channel 和 sync 包中的同步原语。
atomic 包提供了两大类原子操作接口:
- 一类是针对整型变量的,包括有符号整型、无符号整型以及对应的指针类型;
- 一类是针对自定义类型的。
因此,第一类原子操作接口的存在让 atomic 包天然适合去实现某一个共享整型变量的并发同步。
var n1 int64
func addSyncByAtomic(delta int64) int64 {
return atomic.AddInt64(&n1, delta)
}
func readSyncByAtomic() int64 {
return atomic.LoadInt64(&n1)
}
var n2 int64
var rwmu sync.RWMutex
func addSyncByRWMutex(delta int64) {
rwmu.Lock()
n2 += delta
rwmu.Unlock()
}
func readSyncByRWMutex() int64 {
var n int64
rwmu.RLock()
n = n2
rwmu.RUnlock()
return n
}
func BenchmarkAddSyncByAtomic(b *testing.B) {
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
addSyncByAtomic(1)
}
})
}
func BenchmarkReadSyncByAtomic(b *testing.B) {
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
readSyncByAtomic()
}
})
}
func BenchmarkAddSyncByRWMutex(b *testing.B) {
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
addSyncByRWMutex(1)
}
})
}
func BenchmarkReadSyncByRWMutex(b *testing.B) {
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
readSyncByRWMutex()
}
})
}
// 分别在 cpu=2、 8、16、32 的情况下运行上述性能基准测试,得到结果如下:
/*
goos: darwin
goarch: amd64
... ...
BenchmarkAddSyncByAtomic-2 75426774 17.69 ns/op
BenchmarkReadSyncByAtomic-2 1000000000 0.7437 ns/op
BenchmarkAddSyncByRWMutex-2 39041671 30.16 ns/op
BenchmarkReadSyncByRWMutex-2 41325093 28.48 ns/op
BenchmarkAddSyncByAtomic-8 77497987 15.25 ns/op
BenchmarkReadSyncByAtomic-8 1000000000 0.2395 ns/op
BenchmarkAddSyncByRWMutex-8 17702034 67.16 ns/op
BenchmarkReadSyncByRWMutex-8 29966182 40.37 ns/op
BenchmarkAddSyncByAtomic-16 57727968 20.39 ns/op
BenchmarkReadSyncByAtomic-16 1000000000 0.2536 ns/op
BenchmarkAddSyncByRWMutex-16 15029635 78.61 ns/op
BenchmarkReadSyncByRWMutex-16 29722464 40.28 ns/op
BenchmarkAddSyncByAtomic-32 58010497 20.40 ns/op
BenchmarkReadSyncByAtomic-32 1000000000 0.2402 ns/op
BenchmarkAddSyncByRWMutex-32 11748312 93.15 ns/op
BenchmarkReadSyncByRWMutex-32 29845912 40.54 ns/op
*/
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
结论:
- 读写锁的性能随着并发量增大的情况,与前面讲解的 sync.RWMutex 一致;
- 利用原子操作的无锁并发写的性能,随着并发量增大几乎保持恒定;
- 利用原子操作的无锁并发读的性能,随着并发量增大有持续提升的趋势,并且性能是读锁的约 200 倍。
atomic 原子操作的特性:
随着并发量提升,使用 atomic 实现的共享变量的并发读写性能表现更为稳定,尤其是原子读操作,和 sync 包中的读写锁原语比起来,atomic 表现出了更好的伸缩性和高性能。
atomic 包应用场景:
适合一些对性能十分敏感、并发量较大且读多写少的场合。
atomic 原子操作可用来同步的范围有比较大限制,只能同步一个整型变量或自定义类型变量。 如果对一个复杂的临界区数据进行同步,首选依旧是 sync 包。