golang中的data race,mutex,atomic

Data Race 即数据竞争,在多线程,协程状态下读取变量会产生,也就是数据并发问题,会使程序运行结果和预期不一致。

在 golang 中,一般的变量出现了 data race 并不会报错,但是 map 类型变量会出现 fatal error: concurrent map writesfatal error: concurrent map read and map write 的错误,会导致程序崩溃的。

golang 提供了检测 data race 的方法,在运行程序的时候加上 -race 参数即可。

package main

import "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 +0x0
  main.testmap3.func2()
      D:/dev/php/magook/trunk/server/golang/project/demo1/test9.go:21 +0xa7

Previous write at 0x00c000032000 by goroutine 6:
  runtime.mapassign_fast64()
      D:/Go/src/runtime/map_fast64.go:92 +0x0
  main.testmap3.func1()
      D:/dev/php/magook/trunk/server/golang/project/demo1/test9.go:14 +0xa7

Goroutine 7 (running) created at:
  main.testmap3()
      D:/dev/php/magook/trunk/server/golang/project/demo1/test9.go:18 +0xa0
  main.main()
      D:/dev/php/magook/trunk/server/golang/project/demo1/test9.go:29 +0x36

Goroutine 6 (finished) created at:
  main.testmap3()
      D:/dev/php/magook/trunk/server/golang/project/demo1/test9.go:11 +0x7e
  main.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 +0xbc

Previous write at 0x00c000094048 by goroutine 6:
  main.testmap3.func1()
      D:/dev/php/magook/trunk/server/golang/project/demo1/test9.go:14 +0xbc

Goroutine 7 (running) created at:
  main.testmap3()
      D:/dev/php/magook/trunk/server/golang/project/demo1/test9.go:18 +0xa0
  main.main()
      D:/dev/php/magook/trunk/server/golang/project/demo1/test9.go:29 +0x36

Goroutine 6 (finished) created at:
  main.testmap3()
      D:/dev/php/magook/trunk/server/golang/project/demo1/test9.go:11 +0x7e
  main.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 来实现。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值