一、名词解析
1、data race: Any race is a bug
定义: ①多个线程(协程)对于同一个变量、②同时地、③进行读/写操作、并且④至少有一个线程进行写操作。(也就是说,如果所有线程都是只进行读操作,那么将不构成数据争用)
后果: 如果发生了数据争用,读取该变量时得到的值将变得不可知(根据内存模型),使得该多线程程序的运行结果将完全不可预测,有一定可能会导致直接崩溃。
如何防止: 对于有可能被多个线程同时访问的变量使用排他访问控制,具体方法包括使用mutex(互斥量)或者使用atomic变量。
---------------------------------
作者注加一条规则:凡是若干线程(协程)对一个共享变量进行同步操作,且其中有一个是写操作的,那么读/写都要考虑使用原子操作。
---------------------------------
race condition(竞态条件)
读取到数据中间状态的的情形就是 race condition。相对于数据争用(data race),竞态条件(race condition)指的是更加高层次的更加复杂的现象,一般需要在设计并行程序时进行细致入微的分析,才能确定。(也就是隐藏得更深).
定义:受各线程上代码执行的顺序和时机的影响,程序的运行结果产生(预料之外)的变化。
后果:如果存在竞态条件(race condition),多次运行程序对于同一个输入将会有不同的结果,但结果并非完全不可预测,它将由输入数据和各线程的执行顺序共同决定。
如何预防:竞态条件产生的原因很多是对于同一个资源的一系列连续操作并不是原子性的,也就是说有可能在执行的中途被其他线程抢占,同时这个“其他线程”刚好也要访问这个资源。解决方法通常是:将这一系列操作作为一个critical section(临界区)。
2、undefined behavior(未定义行为)
未定义行为是指执行某种计算机代码所产生的结果,这种代码在当前程序状态下的行为在其所使用的语言标准中没有规定。在 Go 的内存模型中,有race的Go程序的行为是未定义行为
3、go run/build -race(开启race检测)
golang在1.1之后引入了竞争检测的概念。我们可以使用go run -race或者go build -race来进行竞争检测。-race选项打开了data race detector用来检查这个错误,而且关闭了相关的编译器优化(作者注:就是为了不让编译器优化代码,能看清楚完整代码)。(原因是)go编译器认为race代码是dead code,可能直接优化掉。
4、原子性
一个或者多个操作在 CPU 执行的过程中不被中断的特性,称为原子性(atomicity)。这些操作对外表现成一个不可分割的整体,他们要么都执行,要么都不执行,外界不会看到他们只执行到一半的状态。
5、原子操作
原子操作(atomic operation)指的是由多步操作组成的一个操作。如果该操作不能原子地执行,则要么执行完所有步骤,要么一步也不执行,不可能只执行所有步骤的一个子集。
在单核系统里,单个的机器指令可以看成是原子操作(如果有编译器优化、乱序执行等情况除外),在单核CPU中, 能够在一个指令中完成的操作都可以看作为原子操作, 因为中断只发生在指令间;
在多核系统中,单个的机器指令就不是原子操作,因为多核系统里是多指令流并行运行的,一个核在执行一个指令时,其他核同时执行的指令有可能操作同一块内存区域,从而出现数据竞争现象。
多核系统中的原子操作通常使用内存栅障(memory barrier)来实现,即一个CPU核在执行原子操作时,其他CPU核必须停止对内存操作或者不对指定的内存进行操作,这样才能避免数据竞争问题。
在多核CPU的时代,体系中运行着多个独立的CPU,即使是可以在单个指令中完成的操作也可能会被干扰. 典型的例子就是decl指令(递减指令), 它细分为三个过程: “读->改->写”, 涉及两次内存操作。如果多个CPU运行的多个进程在同时对同一块内存执行这个指令,那情况是无法预测的。
二、代码分析
race 多协程写分析
golang在1.1之后引入了竞争检测的概念。我们可以使用go run -race 或者 go build -race 来进行竞争检测。
golang语言内部大概的实现就是同时开启多个goroutine执行同一个命令,并且纪录每个变量的状态。
如果使用go run -race 1.go,将出现下列提示:
这个命令输出了WARNING:DATA RACE
总结
本篇主要介绍了一些术语,引用了一条规则:凡是多线程对共享变量涉及到写操作,都要考虑使用原子操作。
其次,介绍了-race选项,可以对代码涉及竞态的问题做个检查。