go 不等于_Go 并发编程——Race Condition

Race Condition 是什么

在正式介绍 race condition 之前我们先来想象一个生活中的情形。语文老师准备在黑板上写一句句子,来考考小朋友的理解能力。她准备写的句子是 “我喜欢吃苹果才怪”,意思是我不喜欢吃苹果。但这个情形中有一个隐含条件,老师会默认为小朋友会在句子写完以后才发言的。如果老师不说明这个隐含条件,小朋友的行为就是不可控的。他们看到黑板上写了“我喜欢吃苹果”几个字可能就会大声发言“老师喜欢吃苹果~”,从而造成了非预期的结果。这种读取到数据中间状态的的情形就是 race condition。

Race Condition 维基百科

有了大致概念以后,我们再来看一下维基百科对 race condition 的定义:

A race condition or race hazard is the condition of an electronics, software, or other system where the system's substantive behavior is dependent on the sequence or timing of other uncontrollable events. It becomes a bug when one or more of the possible behaviors is undesirable.

简单来说 race condition 是系统存在的一种潜在风险,这种风险是由于系统的输出依赖着不可控事件的执行顺序或者执行时间。一旦这些不可控事件不满足预期,系统就会出现 bug。

常见的 Race condition

知道了 race condition 的定义后,我们先来看一段代码考考大家,以下代码是否正确。

代码 A

package main
import "fmt"
type myType struct {
	A int
}
func main() {
	c := make(chan bool)
	x := new(myType)
	go func() {
		x = new(myType) // write to x
		c <- true
	}()
	_ = *x // read from x
	<-c
	fmt.Println("end")
}

这段代码主要做了两件事情,在第 10 行的一个 goroutine 中写入了一个指针到 x,在第 13 行的另一个 goroutine 中读取了该指针中的数据。这是一段抽象出的代码,在真实项目的这段代码对应的是一个 goroutine 的行为,这个 goroutine 会定时地发起 http 请求更新缓存中的数据,然后另一个 goroutine 会不停地读取这个缓存。

那么这段代码到底正确与否呢?答案是:即是错误的又是正确的。下面我们来慢慢分析。

Data Race

首先我们来说说为什么这段代码是错误的。 代码 A 所犯的错误叫 data race , data race 是 race condition 中的一种。我们来看下 go 官方对 data race 的定义。

A data race occurs when two goroutines access the same variable concurrently and at least one of the accesses is a write.

简单来说 data race 就是在两个线程同时访问一块内存并且其中至少有一个写的操作,而上述的代码 A 就是 data race 的标准错误示例。为了更清晰地看到 data race 的表现,我们可以执行一下以下的代码。

代码 B

package main
import "fmt"
func main() {
	var x = [...]int{1, 1, 1, 1, 1, 1}
	c := make(chan int, 100)
	go func() {
		for i := 0; ; i++ {
			if i&1 == 0 {
				x = [...]int{2, 2, 2, 2, 2, 2} // write to x
			} else {
				x = [...]int{1, 1, 1, 1, 1, 1} // write to x
			}
			c <- 0 // let other goroutine see the change of x
		}
	}()
	for {
		d := x // read from x
		if d[0] != d[5] {
			fmt.Println(d)
			panic("error") // proved the copy operation is not atomic
		}
		<-c
	}
}

这段代码做的事情是在一个 goroutine 中循环交叉地给 x 赋值,分别赋值为 "1 1 1 1 1 1 " 和 "2 2 2 2 2 2",然后在另一个 goruntine 中不停读取这个值,如果在该 goroutine 中发现数据不一致就 panic。

实际运行后这段代码很快就会发生 panic,一种输出结果是 [1 1 1 1 2 2] 。所以 data race 可能会造成数据污染,让程序出现未知的输出。甚至在某些情况下 data race 还会出现 undefined behavior,造成更大未知的破坏。

为什么代码 A 又是正确的?

首先在解释代码 A 正确之前我们需要先知道一个概念 pointer size,也就是指针占用的内存大小。

Typically, a pointer is the same size as your system’s architecture, 32 bits on a 32 bit system and 64 bits on a 64 bit system. If the argument is a scalar type (bool, int, float, etc), it’s going to be less than or equal to the size of a pointer. If the argument is a compound type, such as a struct with multiple fields, it’s likely the pointer is smaller.

通常情况下指针的大小是小于等于系统的 machine word 的,比如 32 位的系统指针 size 是小于等于 32 bit,64位的系统指针 size 是小于等于 64 bit。由这一点我们可以知道代码 A 中的指针大小也是小于等于一个 machine word 的。下面是一段简单的代码,可以亲自实践一下查看 pointer 具体的大小。

// 输出 4
// 运行 GOARCH="386"  go run test.go
// 输出 8
// 运行 GOARCH="amd64"  go run test.go
package main
import (
	"fmt"
	"unsafe"
)
func main() {
	var a *int
	fmt.Println(unsafe.Sizeof(a))
}

代码 A 是正确的第二点原因是 CPU 寄存器的大小一定大于一个 machine word 。也就是说数据复制的最小单元大于一个 machine word。所以在复制指针的时候不可能出现复制到一半的中间状态,这也就解释了为什么代码 A 一定是正确的。

为什么代码 A 是正确的却不应该使用呢?

第一点原因是代码 A 出现了 data race,而 data race 的行为对编译器来说是 undefined 的。完全有可能谋个版本的编译器做了特殊的优化,从而导致这部分代码会出错。

第二点原因是代码 A 的用法依赖了硬件的实现,而硬件的实现对于 go 来说是不可控的,也就是 "uncontrollable events"。

第三点原因是人的因素。写下代码 A 的人可能是个资深的程序员十分了解了上述的原理以及风险。但是后续维护的程序员不一定也掌握了这些知识,他们可能会依样画葫芦,针对其他数据结构也都不加同步控制的进行并发读写,从而造成一些可怕的结果。

第四点原因是使用同步控制能够提升代码的可读性,当你的代码在某个地方加了并发控制,比如锁以后,其他程序员立刻就会警觉起来,从而更加注意减少犯错的风险。

第五点原因是 go 有一个 race condition 的检测工具 go run -race xxx.go ,也就是当加上 -race 选项,可以辅助检测可能存在的 race condition。虽然代码 A 是“无害”的,但是这个工具可以立即检测出存在 data race。如果大家长期忽略这个检查,等真正出现 date race 时就有可能会被忽略了,从而造成危险。

正确的做法

所以在并发编程中正确的做法是一定要使用同步控制,比如互斥锁、channel、以及 sync/atomic。个人我很喜欢 atomic 这个包,它的性能是三者最好的。想了解更多强烈推荐看下这篇文章。

总结

说到这里大家肯定也明白了,针对 race condition 不要尝试耍小聪明,老老实实地加上流程控制。最后引用 stackoverflow 上的一个回答来结束这篇文章。

Don't tempt the devil, if there's a race condition, use proper synchronization.

参考资料

https://en.wikipedia.org/wiki/Race_condition​en.wikipedia.org https://golang.org/doc/articles/race_detector.html​golang.org https://medium.com/@vCabbage/go-are-pointers-a-performance-optimization-a95840d3ef85​medium.com https://blog.betacat.io/post/golang-atomic-value-exploration/​blog.betacat.io https://stackoverflow.com/questions/59089844/how-to-create-a-real-race-condition-in-golang​stackoverflow.com
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值