在 go 语言 string 类型思考 中有说到 -race 竞态检测,多个 goroutine 并发读写同一个变量是会触发。竞态竞争导致的问题是:结果不可控,你也无法预料最终的结果是什么。
比较棘手的竞态竞争会发生在一些切片类型上,在遍历读取切片的时候,切片的数据被别的协程更改了。轻则会导致数组下标越界,重则会导致程序 panic。解决的办法一般是加锁。
atomic 原子操作
而 go 其实提供了原子操作的一系列方法,就在 sync/atomic 包下面,在源代码中也经常会看到这些包被使用。在面试的时候,偶尔也会遇到问一些无锁模式编程的理念,原理上也就是用 atomic 的操作。
我们先通过经常竞争的例子来看一下这个问题,这个例子网上比较普遍,for 循环中通过 goroutine 来计数:
func main() {var wg sync.WaitGroupwg.Add(1000)count := 0for i := 0; i < 1000; i++ {go func() {defer wg.Done()count++}()}wg.Wait()fmt.Println(count)
}
执行的时候,我们使用 -race 属性来查看执行结果,会发现,count++的地方存在数据竞争。
我们不使用 count++ 加锁的处理方式,就是使用 sync.Mutex 来加锁,我们直接使用 go 提供的原子方法,我们对代码进行微调。核心点就是 atomic.AddInt64 的方法。多次使用 -race 属性执行,数据竞争已经没有了。
func main() {var wg sync.WaitGroupwg.Add(1000)var count int64 = 0for i := 0; i < 1000; i++ {go func() {defer wg.Done()atomic.AddInt64(&count, 1)}()}wg.Wait()fmt.Println(count)
}
我们查看 atomic 的源码,都是一些基础类型操作,基础数值型以及指针类型。atomic 在此基础上也抽象出了 atomic.Value 的结构,用来原子化操作接口类型。
但 atomic 只能保证它自身提供函数的原子性,一旦脱离了它提供的函数,原子性也就无从谈起。在上面例子的基础上,我们简单做调整:
下面的例子中,atomic 只能保证 Store、Load 操作的原子性,但无法保证 *c++ 的原子性,除非这个操作是深拷贝的覆盖式更新。
另外,我们工作中使用较多的其实是接口类型、切片、map 等复杂结构的数据。拿 map 类型来说,如果需要不同的协程向 map 中写入不同的键值对,atomic 就不能用来保证这个操作。原因也是 *c++ 的原因,map 的赋值过程需要在原子函数之外来处理。
func main() {var wg sync.WaitGroupwg.Add(1000)var count int64 = 0var reletion atomic.Valuereletion.Store(&count)for i := 0; i < 1000; i++ {go func() {defer wg.Done()c := reletion.Load().(*int64)*c++reletion.Store(c)}()}wg.Wait()fmt.Println(count)
}
atomic 扩展
核心就是 go.uber.org/atomic 这个包,在官方 atomic 的基础上做了封装。…