我经常发现这些术语在并发编程的上下文中使用。他们是同一件事还是不同?
Answers:
不,他们不是一回事。它们不是彼此的子集。它们既不是彼此的必要条件也不是充分条件。
数据竞赛的定义非常清楚,因此可以自动进行发现。当来自不同线程的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
。
data race
JLS的实际含义吗?
不,它们是不同的,而且它们都不是一个子集,反之亦然。
术语竞争条件通常与相关的术语数据竞争混淆,后者是在不使用同步来协调对共享非最终字段的所有访问时出现的。如果两个线程都不使用同步,那么只要一个线程写入一个变量可能会再次被另一个线程读取,或者读取一个变量可能最后被另一个线程写入,您就会冒着数据争用的风险。在Java内存模型下,具有数据竞争的代码没有有用的已定义语义。并非所有竞争条件都是数据竞争,也不是所有数据竞争都是竞争条件,但是它们都可能导致并发程序以不可预测的方式失败。
摘自Joshua Bloch&Co .的优秀著作-Java Concurrency in Practice。
TL; DR:数据竞争与竞争条件之间的区别取决于问题制定的性质,以及在未定义行为与定义明确但不确定的行为之间划界的位置。当前的区别是常规的,最好地反映了处理器架构师和编程语言之间的接口。
1.语义学
数据竞争特别是指对同一内存位置的非同步冲突“内存访问”(或动作或操作)。如果内存访问没有冲突,而操作顺序仍然导致不确定的行为,那就是竞争条件。
注意此处的“内存访问”具有特定含义。它们引用“纯”内存加载或存储操作,而未应用任何其他语义。例如,来自一个线程的内存存储区不(有必要)知道将数据写入内存需要多长时间,最后传播到另一个线程。对于另一个示例,通过同一线程将存储器存储到一个位置之前,将另一个存储器存储到另一个位置之前(不一定)不能保证写入存储器的第一个数据位于第二个之前。结果,除非有明确的定义,否则这些“纯内存”访问的顺序是(有必要)不能被“合理化”的,并且任何事情都可能发生。
当通过同步的顺序很好地定义了“内存访问”时,其他语义可以确保,即使内存访问的时间不确定,也可以通过同步来“合理化”它们的顺序。注意,尽管可以合理地确定存储器访问之间的顺序,但是它们不一定是确定的,因此是竞争条件。
2.为什么会有差异?
但是,如果在竞争条件下顺序仍然不确定,为什么还要麻烦将其与数据竞争区分开?原因是在实践中而不是理论上。这是因为在编程语言和处理器体系结构之间的接口中确实存在区别。
由于流水线顺序混乱,推测,多级缓存,cpu-ram互连(尤其是多核等)的性质,现代体系结构中的内存加载/存储指令通常被实现为“纯”内存访问。 。有许多因素导致时间和顺序不确定。强制执行每条存储器指令的指令会产生巨大的代价,尤其是在支持多核的处理器设计中。因此,为排序语义提供了其他指令,例如各种障碍(或栅栏)。
数据争用是处理器指令执行的情况,没有额外的限制来帮助推理冲突的内存访问的顺序。结果不仅是不确定的,而且可能非常奇怪,例如,通过不同的线程两次写入同一字位置可能会导致每次写入一半的字,或者只能根据其本地缓存的值进行操作。-从程序员的角度来看,这是未定义的行为。但是(通常)从处理器架构师的角度出发,对它们进行了很好的定义。
程序员必须有一种方法来推理其代码执行。数据争用是他们无法理解的事情,因此应(通常)避免。这就是为什么足够低级别的语言规范通常将数据争用定义为未定义的行为,与竞争条件的定义良好的内存行为不同。
3.语言记忆模型
不同的处理器可能具有不同的内存访问行为,即处理器内存模型。对于程序员来说,研究每个现代处理器的内存模型然后开发可以从中受益的程序是很尴尬的。如果该语言可以定义一个内存模型,那么该语言的程序始终会按照该内存模型所定义的那样运行,这是理想的。这就是Java和C ++定义其内存模型的原因。确保语言内存模型在不同的处理器体系结构之间强制实施是编译器/运行时开发人员的负担。
也就是说,如果一种语言不想暴露处理器的低级行为(并且愿意牺牲现代体系结构的某些性能优势),则他们可以选择定义一个内存模型来完全隐藏“纯”的细节。内存访问,但对其所有内存操作应用排序语义。然后,编译器/运行时开发人员可以选择将所有内存变量在所有处理器体系结构中均视为易失性。对于这些语言(支持跨线程共享内存),没有数据竞争,但即使使用完全顺序一致性的语言,也可能仍然存在竞争条件。
另一方面,处理器内存模型可以更严格(或更少放松,或更高级别),例如,像早期处理器那样实现顺序一致性。然后,将对所有内存操作进行排序,并且处理器中运行的任何语言都不存在数据争用。
4。结论
回到最初的问题,恕我直言,可以将数据竞争定义为竞争条件的一种特殊情况,并且一级竞争条件可能会变成更高级别的数据竞争。它取决于问题表达的性质,以及在未定义行为与定义明确但不确定的行为之间划界的位置。仅当前的约定定义了语言处理器接口的边界,并不一定意味着总是如此,而且必须如此。但是当前的惯例可能最好地反映了处理器架构师和编程语言之间的最新接口(和智慧)。