概要
实际工作的过程中发现很多小伙伴不是太清楚go语言SDK atomic包下Load变量与Store变量使用场景,已经用法,其实还是不是太了解计算机硬件相关的知识来深入理解SDK设计该方法的初衷
计算机硬件内存
程序执行时,主要是CPU执行程序中的指令,指令运行还需要加载相应的数据;CPU运行的速度很快,如果每次使用IO总线访问内存获取CPU执行所需要的数据,无法将CPU的性能优势发挥到最大;
数据从磁盘读取,加入到RAM主存,线程执行的时候会从CPU缓存获取数据,如果获取不到则把数据从RAM主存加载到CPU缓存中,直接从CPU缓存获取数据可以尽可能的发挥CPU最大性能优势
CPU缓存分为L1、L2、L3,3个级别的缓存,L1级别的缓存就是常说的寄存器缓存,如下图所示
如上图 工作线程/本地线程相当于CPU中的L1/L2/L3 缓存,主内存相当于RAM内存条,就是我们买电脑所说的电脑内存配置,从图中可以看出如果多个线程对同一个共享变量进行操作会存在线程不安全的问题,比如变量a=1,此时A、B两个线程都把a=1读入内存,其中A线程修改变量a=2,此时B线程再次读取a变量,可能a的值还是a=1,这个时候程序就会出现bug,如果是单线程操作一个变量就不会出现这种问题
atomic.Load变量与atomic.Store变量就是解决上述线程不安全问题,类似于Java用volatile关键字来保证并发的可见性(volatile实现内存可见性是通过store和load指令完成的),只不过Java是通过语言层面来解决的,使用起来会更加方便一些,以atomic.LoadInt32()与atomic.StoreInt32()函数为例:
- ** atomic.LoadInt32() **就是原子的把类型为int32的变量从主内存加载到工作内存,防止并发情况下的脏读
- **atomic.StoreInt32()**就是原子的把类型为int32的变量从工作内存同步到主内存,防止其他线程读取到不正确的数据
示例问题代码
package main
import (
"fmt"
)
var x int32 = 1
func storeFunc() {
for i := 0; ; i++ {
if i%2 == 0 {
x = 2
} else {
x = 3
}
}
}
func main() {
go storeFunc()
for {
fmt.Printf("%x\n", x)
}
}
输出结果:
1
1
1
1
1
1
1
1
1
1
1
1
1
结果一直输出1,可以看出这里输出的是错误的脏数据,实际上变量已经变成2或者3了
示例正确代码
package main
import (
"fmt"
"sync/atomic"
)
var x int32 = 1
func storeFunc() {
for i := 0; ; i++ {
if i%2 == 0 {
atomic.StoreInt32(&x, 2)
} else {
atomic.StoreInt32(&x, 3)
}
}
}
func main() {
go storeFunc()
for {
fmt.Printf("%x\n", atomic.LoadInt32(&x))
}
}
输出结果:
2
2
2
3
3
2
3
2
2
3
2
3
2
输出结果可以看出解决了协程可见性的问题
协程的可见性
讲到这里可能有同学会说go语言使用的是协程,上面一直再说线程,两个是不同的概念,实际上道理是一样的
上图可以看出一个线程会间歇执行多个协程(协程的切换时间片是10ms),单个线程执行的多个协程是间歇串行执行的关系,没有并发,而且协程是共享线程的工作内存的,所以单个线程的协程之间不会有可见性问题发生,但是分布在不同线程的协程就不会共享线程的工作内存,他们之间就会有线程不安全,可见性等问题发生
当然go语言协程的GMP模型比上图概念以及工作机制要复杂的多,这里不做深入讲解,但是理解线程不安全以及可见性跟线程是大同小异的