在并发编程的上下文中,“数据竞争”和“竞争条件”实际上是一回事吗

我经常发现这些术语在并发编程的上下文中使用。它们是一样的还是不同的?

41563 次浏览

According to Wikipedia, the term "race condition" has been in use since the days of the first electronic logic gates. In the context of Java, a race condition can pertain to any resource, such as a file, network connection, a thread from a thread pool, etc.

术语“数据竞赛”最好保留其由 JLS定义的特定含义。

最有趣的例子是一个与数据竞争非常相似的竞争条件,但仍然不是一个竞争条件,就像下面这个简单的例子:

class Race {
static volatile int i;
static int uniqueInt() { return i++; }
}

由于 i是易变的,因此不存在数据竞争; 但是,从程序正确性的角度来看,由于两个操作的非原子性,存在一个竞争条件: 读取 i,写入 i+1。多个线程可能从 uniqueInt接收相同的值。

不,这不是一回事。它们不是彼此的子集。它们既不是彼此的必要条件,也不是彼此的充分条件。

The definition of a data race is pretty clear, and therefore, its discovery can be automated. A data race occurs when 2 instructions from different threads access the same memory location, at least one of these accesses is a write and there is no synchronization that is mandating 任何 particular order among these accesses.

竞态条件是语义错误。这是一个缺陷,发生在时间或事件的顺序,导致错误的程序行为。许多竞态条件可能是由数据竞态引起的,但这是不必要的。

考虑下面这个简单的例子,其中 x 是一个共享变量:

Thread 1    Thread 2


lock(l)     lock(l)
x=1         x=2
unlock(l)   unlock(l)

在这个示例中,从线程1和2写入到 x 的操作受到锁的保护,因此它们总是按照在运行时获取锁的顺序强制执行的某种顺序发生。也就是说,不能破坏写的原子性; 在任何执行中,在两个写之间的关系之前总会发生一个变化。我们只是不能预先知道哪个写在另一个之前发生。

写操作之间没有固定的顺序,因为锁不能提供这一点。如果程序的正确性受到损害,比如当线程2写入 x 之后是线程1中写入 x 之后,我们说存在竞争条件,尽管从技术上讲没有数据竞争。

检测竞态条件要比检测数据竞态有用得多,但是这也很难实现。

构造反向示例也很简单。通过一个简单的银行交易例子,这个博客文章也很好地解释了这种差异。

不,它们是不同的,它们都不是一个 subset,反之亦然。

术语竞争条件常常与相关术语数据混淆 当不使用同步来协调所有 访问共享的非最终字段 线程写入下一个可能被另一个线程读取的变量 读取可能上次被另一个线程写入的变量 如果两个线程都不使用同步; 具有数据竞争的代码已经 在 Java 内存模型下没有有用的定义语义 条件是数据竞争,并非所有的数据竞争都是竞争条件, 但它们都会导致并发程序在不可预测的情况下失败 方式。

摘自优秀的书 -实践中的 Java 并发作者: Brian Goetz & Co。

数据竞争和竞争条件之间的区别取决于问题表述的性质,以及在哪里划定未定义行为和定义明确但不确定的行为之间的界限。当前的区别是传统的,最好地反映了处理器架构师和编程语言之间的接口。

1. 语义学

数据竞争特指对同一内存位置的非同步冲突的“内存访问”(或动作或操作)。如果在内存访问中没有冲突,而仍然存在由操作顺序引起的不确定行为,则为竞态条件。

注意“内存访问”在这里有特定的含义。它们引用“纯”内存加载或存储操作,而不应用任何其他语义。例如,来自一个线程的内存存储并不(必然)知道将数据写入内存并最终传播到另一个线程需要多长时间。另一个例子是,通过同一个线程将内存存储到一个位置,然后再将另一个存储到另一个位置,这并不(必然)保证在内存中写入的第一个数据在第二个数据之前。因此,这些纯内存访问的顺序(必然)不能是 "reasoned",除非另有明确定义,否则任何事情都可能发生。

当“内存访问”在通过同步进行排序方面得到很好的定义时,额外的语义可以确保,即使内存访问的时间不确定,它们通过同步进行排序的顺序也可以是 “理智”。注意,虽然可以推断内存访问之间的顺序,但它们不一定是确定的,因此存在竞争条件。

2. 为什么会有差别?

但是,如果顺序在竞态条件下仍然是不确定的,为什么要费心将它与数据竞态区分开来呢?原因在于实践而非理论。这是因为在编程语言和处理器体系结构之间的接口中确实存在区别。

A memory load/store instruction in modern architecture is usually implemented as "pure" memory access, due to the nature of out-of-order pipeline, speculation, multi-level of cache, cpu-ram interconnection, especially multi-core, etc. There are lots of factors leading to indeterminate timing and ordering. To enforce ordering for every memory instruction incurs huge penalty, especially in a processor design that supports multi-core. So the ordering semantics are provided with additional instructions like various barriers (or fences).

数据竞争是处理器指令执行的情况,没有额外的栅栏来帮助推理冲突的内存访问的顺序。结果不仅不确定,而且可能非常奇怪,例如,不同线程对同一个单词位置的两次写操作可能导致每次写入单词的一半,或者可能只对它们的本地缓存值进行操作。从程序员的角度来看,这些都是未定义行为。但是从处理器架构师的角度来看,它们(通常)定义得很好。

程序员必须有一种方法来 原因他们的代码执行。数据竞争是他们无法理解的,因此应该总是避免(通常)。这就是为什么低级别的语言规范通常将数据竞争定义为未定义行为,不同于定义良好的竞争条件的内存行为。

3. 语言记忆模型

不同的处理器可能有不同的内存访问行为,即处理器内存模型。对于程序员来说,研究每一个现代处理器的内存模型,然后开发能够从中受益的程序是很尴尬的。如果该语言能够定义一个内存模型,使得该语言的程序始终按照内存模型所定义的那样行为,那么这是可取的。这就是为什么 Java 和 C + + 定义了它们的内存模型。确保语言内存模型跨不同的处理器体系结构执行是编译器/运行时开发人员的负担。

也就是说,如果一种语言不想暴露处理器的低级行为(并且愿意牺牲现代架构的某些性能优势) ,他们可以选择定义一种内存模型,完全隐藏“纯”内存访问的细节,但是为他们所有的内存操作应用排序语义。然后,编译器/运行时开发人员可以选择将所有处理器架构中的每个内存变量视为可变的。对于这些语言(支持跨线程共享内存) ,没有数据竞争,但仍可能存在竞争条件,即使语言具有完全顺序一致性。

另一方面,处理器内存模型可以更加严格(或者不那么宽松,或者在更高的级别) ,例如,像早期的处理器那样实现循序一致性。然后对所有内存操作进行排序,处理器中运行的任何语言都不存在数据竞争。

4. 结论

回到最初的问题,恕我直言,将数据竞争定义为竞争条件的特殊情况是可以的,一个级别上的竞争条件可以成为更高级别上的数据竞争。这取决于问题表述的性质,以及在哪里划定未定义行为和定义明确但不确定的行为之间的界限。仅仅是当前的约定定义了语言处理器接口的边界,并不一定意味着总是而且必须是这样; 但是当前的约定可能最好地反映了处理器架构师和编程语言之间的最先进的接口(和智慧)。