在并发编程的上下文中,"数据竞争"和"竞争条件"实际上是相同的

Inq*_*ive 82 language-agnostic concurrency multithreading data-race

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

Bar*_*kci 123

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

数据竞争的定义非常清楚,因此,它的发现可以自动化.当来自不同线程的2个指令访问相同的存储器位置时,发生数据竞争,这些访问中的至少一个是写入,并且没有同步强制要求这些访问中的任何特定顺序.

竞争条件是语义错误.这是导致错误的程序行为的事件的时间或顺序中发生的缺陷.数据竞赛可能导致许多竞争条件,但这不是必需的.

考虑以下简单示例,其中x是共享变量:

Thread 1    Thread 2

 lock(l)     lock(l)
 x=1         x=2
 unlock(l)   unlock(l)
Run Code Online (Sandbox Code Playgroud)

在此示例中,来自线程1和2的对x的写入受锁保护,因此它们总是按照在运行时获取锁的顺序强制执行的顺序发生.也就是说,写入的原子性不能被破坏; 在任何执行中,两个写入之间的关系总会发生.我们只是不知道哪个写在另一个先验之前发生.

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

检测竞争条件远比数据竞争更有用; 然而,这也很难实现.

构建反向示例也是微不足道的.这篇博文还通过简单的银行交易示例很好地解释了这种差异.

  • @josinalvo:它是数据竞争的技术定义的工件.关键点是在两次访问之间,将有一个锁定释放和一个锁定获取(对于任何一个可能的命令).根据定义,锁定释放和锁定获取在两次访问之间建立了一个顺序,因此没有数据争用. (5认同)
  • “数据竞争(...)没有同步要求这些访问之间有任何特定的顺序。” 我有一点困惑。在您的示例中,操作可能以两种顺序发生(=1 然后=2,或者相反)。为什么这不是一场数据竞赛? (3认同)
  • @BarisKasikci 恐怕我仍然不太相信它们在象牙塔并发理论模型之外有用。对我来说,我想说的是大多数实际开发人员,竞争条件是有用且直观的日常概念。C 和 C++ 内存模型实际上将“数据竞争”定义为竞争条件的子集,上次我检查过,这更有意义。 (2认同)
  • @Noldorin 这是维基百科上的一句话,而不是标准中的定义。您能否从标准中的内存模型讨论中指出您的主张“C 和 C++ 内存模型实际上将“数据竞争”定义为竞争条件的子集,我上次检查”的参考? (2认同)

Mar*_*nik 19

根据维基百科,自第一个电子逻辑门开始以来,术语"竞争条件"一直在使用.在Java的上下文中,竞争条件可以与任何资源相关,例如文件,网络连接,来自线程池的线程等.

术语"数据竞争"最好保留其由JLS定义的特定含义.

最有趣的情况是竞争条件非常类似于数据竞争,但仍然不是一个,就像在这个简单的例子中:

class Race {
  static volatile int i;
  static int uniqueInt() { return i++; }
}
Run Code Online (Sandbox Code Playgroud)

由于i是不稳定的,没有数据竞争; 但是,从程序正确性的角度来看,由于两个操作的非原子性存在竞争条件:读取i,写入i+1.多个线程可以从中接收相同的值uniqueInt.


Shi*_*han 6

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

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

摘自Brian Goetz & Co 所著的优秀书籍《Java Concurrency in Practice》。


Xia*_* Li 5

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

1. 语义

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

注意这里的“内存访问”有特定的含义。它们指的是“纯”内存加载或存储操作,没有应用任何额外的语义。例如,来自一个线程的内存存储(不一定)知道将数据写入内存并最终传播到另一个线程需要多长时间。再例如,由同一线程在另一个位置存储到另一个位置之前的内存存储并不能(必然)保证写入内存中的第一个数据在第二个之前。结果,这些纯内存访问的顺序(必然)不能被“合理化”,任何事情都可能发生,除非另有明确定义。

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

2. 为何不同?

但是如果顺序在竞争条件下仍然是不确定的,为什么要费心把它和数据竞争区分开来呢?原因是实际的而不是理论上的。这是因为编程语言和处理器架构之间的接口确实存在区别。

由于乱序流水线、推测、多级缓存、cpu-ram 互连,尤其是多核等特性,现代架构中的内存加载/存储指令通常被实现为“纯”内存访问. 有很多因素导致不确定的时间和顺序。强制对每条内存指令进行排序会导致巨大的损失,尤其是在支持多核的处理器设计中。因此,排序语义提供了额外的指令,如各种障碍(或围栏)。

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

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

3. 语言记忆模型

不同的处理器可能有不同的内存访问行为,即处理器内存模型。程序员研究每个现代处理器的内存模型然后开发可以从中受益的程序是很尴尬的。如果语言可以定义内存模型,那么该语言的程序总是按照内存模型定义的预期运行,这是可取的。这就是 Java 和 C++ 定义了它们的内存模型的原因。编译器/运行时开发人员的负担是确保跨不同的处理器架构强制执行语言内存模型。

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

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

4。结论

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


yoA*_*ex5 5

数据竞争和竞争条件

【原子性、可见性、有序性】

在我看来,这绝对是两件不同的事情。

Data races是一种在多个线程之间共享相同内存的情况(至少其中一个线程更改它(写访问))而不同步

Race condition是一种使用相同共享资源的不同步代码块(可能相同)在不同线程上同时运行且结果不可预测的情况。

Race condition例子:

//increment variable
1. read variable
2. change variable
3. write variable

//cache mechanism
1. check if exists in cache and if not
2. load
3. cache
Run Code Online (Sandbox Code Playgroud)

解决方案:

Data racesRace condition都是原子性问题,可以通过同步机制来解决。

  • Data races- 当对共享变量的写访问将被同步
  • Race condition- 当代码块作为原子操作运行时