Data Race 即数据竞争,在多线程,协程状态下读取变量会产生,也就是数据并发问题,会使程序运行结果和预期不一致。
在 golang 中,一般的变量出现了 data race 并不会报错,但是 map 类型变量会出现 fatal error: concurrent map writes
和 fatal error: concurrent map read and map write
的错误,会导致程序崩溃的。
golang 提供了检测 data race 的方法,在运行程序的时候加上 -race
参数即可。
package mainimport "sync"var gw sync.WaitGroup
func testmap3() {
m := map[int]int{
}gw.Add(2)go func() {
defer gw.Done()for i := 0; i < 100; i++ {
m[1] = 1}}()go func() {
defer gw.Done()for i := 0; i < 100; i++ {
m[1] = 1}}()gw.Wait()
}func main() {
testmap3()
}
以上程序运行大部分是不会报错的,但是它却有竞争。
go run -race test9.go==================
WARNING: DATA RACE
Write at 0x00c000032000 by goroutine 7:runtime.mapassign_fast64()D:/Go/src/runtime/map_fast64.go:92 +0x0main.testmap3.func2()D:/dev/php/magook/trunk/server/golang/project/demo1/test9.go:21 +0xa7Previous write at 0x00c000032000 by goroutine 6:runtime.mapassign_fast64()D:/Go/src/runtime/map_fast64.go:92 +0x0main.testmap3.func1()D:/dev/php/magook/trunk/server/golang/project/demo1/test9.go:14 +0xa7Goroutine 7 (running) created at:main.testmap3()D:/dev/php/magook/trunk/server/golang/project/demo1/test9.go:18 +0xa0main.main()D:/dev/php/magook/trunk/server/golang/project/demo1/test9.go:29 +0x36Goroutine 6 (finished) created at:main.testmap3()D:/dev/php/magook/trunk/server/golang/project/demo1/test9.go:11 +0x7emain.main()D:/dev/php/magook/trunk/server/golang/project/demo1/test9.go:29 +0x36
==================
==================
WARNING: DATA RACE
Write at 0x00c000094048 by goroutine 7:main.testmap3.func2()D:/dev/php/magook/trunk/server/golang/project/demo1/test9.go:21 +0xbcPrevious write at 0x00c000094048 by goroutine 6:main.testmap3.func1()D:/dev/php/magook/trunk/server/golang/project/demo1/test9.go:14 +0xbcGoroutine 7 (running) created at:main.testmap3()D:/dev/php/magook/trunk/server/golang/project/demo1/test9.go:18 +0xa0main.main()D:/dev/php/magook/trunk/server/golang/project/demo1/test9.go:29 +0x36Goroutine 6 (finished) created at:main.testmap3()D:/dev/php/magook/trunk/server/golang/project/demo1/test9.go:11 +0x7emain.main()D:/dev/php/magook/trunk/server/golang/project/demo1/test9.go:29 +0x36
==================
Found 2 data race(s)
exit status 66
但是总有人以为,不加锁导致的问题最多就是读取的数据是修改前的数据,不能保证原子性罢了。是这样的吗?从上面的输出来看,似乎也差不多,其实这些都是典型的误解。
有些朋友可能不知道,在 Go(甚至是大部分语言)中,一条普通的赋值语句其实并不是一个原子操作(语言规范同样没有定义 i++
是原子操作, 任何变量的赋值都不是原子操作) 。例如,在 32 位机器上写 int64 类型的变量是有中间状态的,它会被拆成两次写操作 MOV —— 写低 32 位和写高 32 位,如下图所示:
如果一个线程刚写完低 32 位,还没来得及写高 32 位时,另一个线程读取了这个变量,那它得到的就是一个毫无逻辑的中间变量,这很有可能使我们的程序出现诡异的 Bug。
解决 race 的问题时,无非就是上锁。可能很多人都听说过一个高逼格的词叫「无锁队列」。 都一听到加锁就觉得很 low,那无锁又是怎么一回事?其实就是利用 atomic 特性,那 atomic 会比 mutex 有什么好处呢? Benign Data Races: What Could Possibly Go Wrong? 的作者总结了这两者的一个区别:
Mutexes do no scale. Atomic loads do.
mutex 由操作系统实现,而 atomic 包中的原子操作则由底层硬件直接提供支持。在 CPU 实现的指令集里,有一些指令被封装进了 atomic 包,这些指令在执行的过程中是不允许中断(interrupt)的,因此原子操作可以在 lock-free 的情况下保证并发安全,并且它的性能也能做到随 CPU 个数的增多而线性扩展。
若实现相同的功能,后者通常会更有效率,并且更能利用计算机多核的优势。所以,以后当我们想并发安全的更新一些变量的时候,我们应该优先选择用 atomic 来实现。