我经常发现这些术语在并发编程的上下文中使用。它们是相同的还是不同的?
4 回答
不,它们不是一回事。它们不是彼此的子集。它们既不是彼此的必要条件,也不是充分条件。
数据竞赛的定义非常明确,因此它的发现可以自动化。当来自不同线程的 2 条指令访问同一内存位置时,就会发生数据竞争,这些访问中至少有一个是写入,并且在这些访问之间没有强制任何特定顺序的同步。
竞争条件是语义错误。这是发生在时间或事件顺序中的缺陷,会导致错误的程序行为。许多竞争条件可能是由数据竞争引起的,但这不是必需的。
考虑以下简单示例,其中 x 是共享变量:
Thread 1 Thread 2
lock(l) lock(l)
x=1 x=2
unlock(l) unlock(l)
在此示例中,线程 1 和 2 对 x 的写入受到锁的保护,因此它们总是以某种顺序发生,该顺序由运行时获取锁的顺序强制执行。也就是说,写入的原子性不能被破坏;在任何执行中,两个写入之间的关系总是有一个发生之前。我们只是无法知道哪个写入发生在另一个之前。
写入之间没有固定的顺序,因为锁无法提供这一点。如果程序的正确性受到损害,例如当线程 2 对 x 的写入之后是线程 1 中对 x 的写入时,我们说存在竞争条件,尽管从技术上讲不存在数据竞争。
检测竞争条件比检测数据竞争有用得多;然而这也很难实现。
构建反向示例也很简单。这篇博文也很好地解释了差异,并以一个简单的银行交易示例为例。
根据 Wikipedia 的说法,“竞态条件”一词自第一个电子逻辑门出现以来就一直在使用。在 Java 的上下文中,竞争条件可以与任何资源有关,例如文件、网络连接、线程池中的线程等。
术语“数据竞争”最好保留其由JLS定义的特定含义。
最有趣的情况是与数据竞争非常相似的竞争条件,但仍然不是,就像在这个简单的例子中一样:
class Race {
static volatile int i;
static int uniqueInt() { return i++; }
}
由于i
是易失性的,因此没有数据竞争;但是,从程序正确性的角度来看,由于两个操作的非原子性存在竞争条件: read i
, write i+1
。多个线程可能会从uniqueInt
.
TL;DR:数据竞争和竞争条件之间的区别取决于问题表述的性质,以及在何处划定未定义行为与明确定义但不确定的行为之间的界限。当前的区别是传统的,最能反映处理器架构和编程语言之间的接口。
1. 语义
数据竞争具体是指对同一内存位置的非同步冲突“内存访问”(或动作或操作)。如果内存访问没有冲突,而仍然存在由操作顺序引起的不确定行为,那就是竞争条件。
注意这里的“内存访问”有特定的含义。它们指的是“纯”内存加载或存储操作,没有应用任何额外的语义。例如,来自一个线程的内存存储不(必然)知道将数据写入内存需要多长时间,并最终传播到另一个线程。再举一个例子,在另一个位置由同一线程存储到另一个位置之前,内存存储到一个位置并不(必然)保证写入内存的第一个数据在第二个之前。结果,这些纯内存访问的顺序不能(必然)被“推理”,除非另有明确定义,否则任何事情都可能发生。
当“内存访问”在通过同步的顺序方面得到很好的定义时,附加语义可以确保即使内存访问的时间不确定,它们的顺序也可以通过同步来“推理” 。请注意,尽管可以推断内存访问之间的顺序,但它们不一定是确定的,因此存在竞争条件。
2. 为什么不一样?
但是,如果在竞争条件下订单仍然是不确定的,为什么还要将其与数据竞争区分开来呢?原因在于实践而非理论。这是因为编程语言和处理器架构之间的接口确实存在区别。
由于乱序流水线、推测、多级缓存、cpu-ram 互连,尤其是多核等特性,现代架构中的内存加载/存储指令通常实现为“纯”内存访问. 有很多因素导致不确定的时间和顺序。对每条内存指令强制排序会导致巨大的损失,尤其是在支持多核的处理器设计中。因此,为排序语义提供了额外的指令,如各种障碍(或栅栏)。
数据竞争是处理器指令执行的情况,没有额外的栅栏来帮助推理冲突内存访问的顺序。结果不仅不确定,而且可能非常奇怪,例如,不同线程对同一单词位置的两次写入可能导致每次写入一半单词,或者可能仅对其本地缓存的值进行操作。-- 从程序员的角度来看,这些是未定义的行为。但从处理器架构师的角度来看,它们(通常)是很好定义的。
程序员必须有一种方法来推理他们的代码执行。数据竞赛是他们无法理解的事情,因此应该始终避免(通常)。这就是为什么足够低级别的语言规范通常将数据竞争定义为未定义的行为,与竞争条件的明确定义的内存行为不同。
3. 语言记忆模型
不同的处理器可能有不同的内存访问行为,即处理器内存模型。程序员研究每个现代处理器的内存模型然后开发可以从中受益的程序是很尴尬的。如果该语言可以定义一个内存模型,以便该语言的程序总是按照内存模型定义的预期行为,那将是可取的。这就是为什么 Java 和 C++ 定义了它们的内存模型。编译器/运行时开发人员有责任确保语言内存模型在不同的处理器体系结构中得到执行。
也就是说,如果一种语言不想暴露处理器的低级行为(并且愿意牺牲现代架构的某些性能优势),他们可以选择定义一个完全隐藏“纯”细节的内存模型内存访问,但对其所有内存操作应用排序语义。然后编译器/运行时开发人员可以选择在所有处理器架构中将每个内存变量视为易失性。对于这些语言(支持跨线程共享内存),没有数据竞争,但可能仍然是竞争条件,即使使用完全顺序一致性的语言。
另一方面,处理器内存模型可以更严格(或不那么宽松,或更高级别),例如,像早期处理器那样实现顺序一致性。然后所有内存操作都是有序的,处理器中运行的任何语言都不存在数据竞争。
4。结论
回到最初的问题,恕我直言,可以将数据竞争定义为竞争条件的一种特殊情况,并且一个级别的竞争条件可能会成为更高级别的数据竞争。这取决于问题表述的性质,以及在何处划定未定义行为和明确定义但不确定的行为之间的界限。只是当前的约定定义了语言处理器接口的边界,并不一定意味着总是并且必须如此;但是当前的约定可能最好地反映了处理器架构师和编程语言之间最先进的接口(和智慧)。
不,它们是不同的,它们都不是其中一个的子集,反之亦然。
术语竞争条件经常与相关术语数据竞争相混淆,数据竞争是在不使用同步来协调对共享非最终字段的所有访问时出现的。每当一个线程写入一个可能下一次被另一个线程读取的变量,或者如果两个线程都不使用同步时读取一个可能最后被另一个线程写入的变量,你就会冒数据竞争的风险;具有数据竞争的代码在 Java 内存模型下没有有用的定义语义。不是所有的竞争条件都是数据竞争,也不是所有的数据竞争都是竞争条件,但它们都可能导致并发程序以不可预知的方式失败。
摘自Joshua Bloch & Co. 的优秀书籍 - Java Concurrency in Practice。