在并发编程的上下文中,“数据竞赛”和“竞赛条件”是否实际上是同一件事?


Answers:


138

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

数据竞赛的定义非常清楚,因此可以自动进行发现。当来自不同线程的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的写入之后,则我们说存在竞争条件,尽管从技术上讲没有数据竞争。

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

构造相反的示例也是微不足道的。这篇博客文章还通过一个简单的银行交易示例很好地解释了差异。


“数据争用(...)没有同步,这些同步要求这些访问之间的任何特定顺序。” 我有点困惑。在您的示例中,操作可能会以两种顺序发生(先是= 1然后= 2,或者相反)。为什么不是数据竞赛?
josinalvo

5
@josinalvo:它是数据争用技术定义的产物。关键点是在两次访问之间,将有一个锁释放和一个锁获取(对于任一可能的命令)。根据定义,锁释放和锁获取会在两次访问之间建立顺序,因此不会发生数据争用。
Baris Kasikci 2015年

同步从未在操作之间强制要求任何特定的顺序,因此这不是表达它的一种非常幸运的方式。另一方面,JMM指定对于每个读取操作,即使在数据竞争中,也必须遵守一定的写入操作。很难避免显式地提到“先发生”和“同步”顺序,但是即使JLS定义在提到“ 先发生”之前也是错误的:按照其定义,两个并发的易失性写入构成数据竞争。
Marko Topolnik

就我而言,@ BarisKasikci“建立订单”没有任何实际含义。他们只是狡猾的话。老实说,我不认为“数据竞赛”是一个远程有用的概念,因为从字面上看,可以将多个线程访问的每个内存位置都视为“数据竞赛”
Noldorin

发行获得对始终会建立订单。一般的解释很长,但是一个简单的例子是信号等待对。@Noldorin“建立顺序”是指先发生顺序,这是并发理论(请参阅Lamport关于关系发生的先例的开创性论文)和分布式系统的关键概念。数据竞争是一个有用的概念,因为它们的存在会带来很多问题(例如,按照C ++内存模型的未定义语义,Java中非常复杂的语义等)。它们的发现和消除构成了研究和实践中的大量文献。
Baris Kasikci

20

根据Wikipedia的说法,自从第一批电子逻辑门问世以来,就一直在使用“竞赛条件”一词。在Java的上下文中,竞争条件可以与任何资源有关,例如文件,网络连接,线程池中的线程等。

术语“数据竞赛”最适合保留JLS定义的特定含义。

最有趣的情况是一种竞争条件,它与数据竞争非常相似,但仍然不是一个竞争条件,如以下简单示例所示:

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

由于i是易失性的,所以没有数据竞争。但是,从程序正确性的角度来看,由于两个操作的非原子性,因此存在竞争条件:read i,write i+1。多个线程可能会从接收相同的值uniqueInt


1
您可以在答案中插入一行描述data raceJLS的实际含义吗?
极客

@geek单词“ JLS”是指向JLS相关部分的超链接。
Marko Topolnik 2013年

@MarkoTopolnik这个例子让我有点困惑。您能否解释一下:“由于我是易变的,因此没有数据竞争”?波动率仅确保其可见,但仍然:1)它不同步,并且多个线程可以同时读取/写入,2)它是共享的非最终字段,因此,根据Java Concurrency in Practice(也引用在下面) ,这是数据竞争而非竞争状态,不是吗?
aniliitb10

@ aniliitb10而不是依赖脱离上下文的二手引号,您应该查看答案中链接的JLS第17.4节。访问volatile变量是第17.4.2节中定义的同步操作
Marko Topolnik,

@ aniliitb10 Votaltile不会引起数据争用,因为可以对它们的访问进行排序。也就是说,您可以以此方式推论其顺序,从而导致不同的结果。借助数据竞争,您无法推理顺序。例如,每个线程的i ++操作可能仅在它们各自的本地缓存值i上发生。在全球范围内,您无法(从程序员的角度)对这些操作进行排序-除非您具有某种语言存储模型。
萧锋丽

3

不,它们是不同的,而且它们都不是一个子集,反之亦然。

术语竞争条件通常与相关的术语数据竞争混淆,后者是在不使用同步来协调对共享非最终字段的所有访问时出现的。如果两个线程都不使用同步,那么只要一个线程写入一个变量可能会再次被另一个线程读取,或者读取一个变量可能最后被另一个线程写入,您就会冒着数据争用的风险。在Java内存模型下,具有数据竞争的代码没有有用的已定义语义。并非所有竞争条件都是数据竞争,也不是所有数据竞争都是竞争条件,但是它们都可能导致并发程序以不可预测的方式失败。

摘自Joshua Bloch&Co .的优秀著作-Java Concurrency in Practice。


请注意,该问题具有与语言无关的标记。
martinkunev

1

TL; DR:数据竞争与竞争条件之间的区别取决于问题制定的性质,以及在未定义行为与定义明确但不确定的行为之间划界的位置。当前的区别是常规的,最好地反映了处理器架构师和编程语言之间的接口。

1.语义学

数据竞争特别是指对同一内存位置的非同步冲突“内存访问”(或动作或操作)。如果内存访问没有冲突,而操作顺序仍然导致不确定的行为,那就是竞争条件。

注意此处的“内存访问”具有特定含义。它们引用“纯”内存加载或存储操作,而未应用任何其他语义。例如,来自一个线程的内存存储区不(有必要)知道将数据写入内存需要多长时间,最后传播到另一个线程。对于另一个示例,通过同一线程将存储器存储到一个位置之前,将另一个存储器存储到另一个位置之前(不一定)不能保证写入存储器的第一个数据位于第二个之前。结果,除非有明确的定义,否则这些“纯内存”访问的顺序是(有必要)不能被“合理化”的,并且任何事情都可能发生。

当通过同步的顺序很好地定义了“内存访问”时,其他语义可以确保,即使内存访问的时间不确定,也可以通过同步来“合理化”它们的顺序。注意,尽管可以合理地确定存储器访问之间的顺序,但是它们不一定是确定的,因此是竞争条件。

2.为什么会有差异?

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

由于流水线顺序混乱,推测,多级缓存,cpu-ram互连(尤其是多核等)的性质,现代体系结构中的内存加载/存储指令通常被实现为“纯”内存访问。 。有许多因素导致时间和顺序不确定。强制执行每条存储器指令的指令会产生巨大的代价,尤其是在支持多核的处理器设计中。因此,为排序语义提供了其他指令,例如各种障碍(或栅栏)。

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

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

3.语言记忆模型

不同的处理器可能具有不同的内存访问行为,即处理器内存模型。对于程序员来说,研究每个现代处理器的内存模型然后开发可以从中受益的程序是很尴尬的。如果该语言可以定义一个内存模型,那么该语言的程序始终会按照该内存模型所定义的那样运行,这是理想的。这就是Java和C ++定义其内存模型的原因。确保语言内存模型在不同的处理器体系结构之间强制实施是编译器/运行时开发人员的负担。

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

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

4。结论

回到最初的问题,恕我直言,可以将数据竞争定义为竞争条件的一种特殊情况,并且一级竞争条件可能会变成更高级别的数据竞争。它取决于问题表达的性质,以及在未定义行为与定义明确但不确定的行为之间划界的位置。仅当前的约定定义了语言处理器接口的边界,并不一定意味着总是如此,而且必须如此。但是当前的惯例可能最好地反映了处理器架构师和编程语言之间的最新接口(和智慧)。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.