什么是比赛条件?

编写多线程应用程序时,遇到的最常见问题之一是竞争条件。

我对社区的问题是:

什么是比赛条件?您如何检测到它们?您如何处理它们?最后,如何防止它们发生?

答案

if (x == 5) // The "Check"
{
   y = x * 2; // The "Act"

   // If another thread changed x in between "if (x == 5)" and "y = x * 2" above,
   // y will not be equal to 10.
}
// Obtain lock for x
if (x == 5)
{
   y = x * 2; // Now, nothing can change x until the lock is released. 
              // Therefore y = 10
}
// release lock for x
for ( int i = 0; i < 10000000; i++ )
{
   x = x + 1; 
}
Retrieve the value of x
Add 1 to this value
Store this value to x
Thread 1: reads x, value is 7
Thread 1: add 1 to x, value is now 8
Thread 2: reads x, value is 7
Thread 1: stores 8 in x
Thread 2: adds 1 to x, value is now 8
Thread 2: stores 8 in x
for ( int i = 0; i < 10000000; i++ )
{
   //lock x
   x = x + 1; 
   //unlock x
}

什么是比赛条件?

您打算在下午 5 点看电影。您在下午 4 点询问门票的供应情况。该代表说,他们有空。放映前 5 分钟,您可以放松身心并到达售票窗口。我敢肯定,您可以猜测会发生什么:这是一间完整的房子。这里的问题在于检查和操作之间的持续时间。您在 4 咨询并在 5 采取行动。与此同时,其他人则抢了票。那是比赛条件 - 特别是比赛条件的 “先检查后行动” 场景。

您如何检测到它们?

宗教代码审查,多线程单元测试。没有捷径。很少有 Eclipse 插件出现,但是还没有稳定的东西。

您如何处理和预防它们?

最好的办法是创建无副作用的无状态函数,并尽可能多地使用不可变对象。但这并不总是可能的。因此,使用 java.util.concurrent.atomic,并发数据结构,正确的同步以及基于 actor 的并发性将有所帮助。

最佳的并发资源是 JCIP。您还可以在此处获得有关上述说明的更多详细信息

竞争条件和数据竞争之间存在重要的技术差异。大多数答案似乎都假设这些术语是等效的,但事实并非如此。

当 2 条指令访问相同的存储器位置时,将发生数据争用,这些访问中的至少一个是写操作,并且在这些访问之间进行排序之前不会发生任何情况 。现在,关于在顺序之前发生的事件的争论很多,但是通常在同一锁定变量上的 ulock-lock 对和在同一条件变量上的 wait-signal 对会导致发生先于顺序。

竞争条件是语义错误。这是在事件的时间安排或顺序中发生的缺陷,导致错误的程序行为

许多竞争条件可能是(实际上是)数据竞争引起的,但这不是必需的。实际上,数据争用和竞争条件既不是彼此的必要条件也不是充分条件。 这篇博客文章还通过一个简单的银行交易示例很好地解释了差异。这是另一个简单的例子 ,解释了两者之间的区别。

现在我们已经确定了术语,让我们尝试回答原始问题。

由于种族条件是语义错误,因此没有检测它们的通用方法。这是因为在一般情况下,无法使用自动的 oracle 来区分正确与错误程序行为。种族检测是一个不确定的问题。

另一方面,数据竞争具有不一定与正确性相关的精确定义,因此可以检测到它们。数据竞争检测器有很多类型(静态 / 动态数据竞争检测,基于锁集的数据竞争检测,基于事前发生的数据竞争检测,混合数据竞争检测)。最先进的动态数据竞争检测器是ThreadSanitizer ,在实践中效果很好。

通常,处理数据争用需要一定的编程纪律,以在共享数据访问之间的边缘发生之前(在开发过程中,或使用上述工具检测到它们之前)诱发发生。这可以通过锁,条件变量,信号量等来完成。但是,也可以采用不同的编程范例,例如消息传递(而不是共享内存)来避免构造过程中的数据争用。

规范的定义是 “ 当两个线程同时访问内存中的相同位置,并且其中至少一个访问是写操作时” 。在这种情况下,“阅读器” 线程可能会获得旧值或新值,具体取决于哪个线程 “赢得了比赛”。这并不总是一个错误 - 实际上,某些真正毛茸茸的低级算法是故意这样做的 - 但通常应避免这样做。 @Steve Gury 给出了一个很好的例子,说明何时可能出现问题。

if( object.a != 0 )
    object.avg = total / object.a
object.a = 0

竞争状况不仅与软件有关,而且与硬件有关。实际上,该术语最初是由硬件行业创造的。

根据维基百科

该术语起源于两个信号相互竞争首先影响输出的想法。

逻辑电路中的竞争条件:

在此处输入图片说明

软件行业不加修改地使用了这个术语,这使它有点难以理解。

您需要进行一些替换以将其映射到软件世界:

  • “两个信号” =>“两个线程” /“两个进程”
  • “影响输出” =>“影响某些共享状态”

因此,软件行业的竞争状况意味着 “两个线程” /“两个进程” 相互竞争以 “影响某个共享状态”,并且共享状态的最终结果将取决于一些细微的时序差异,这可能是由某些特定原因引起的。线程 / 进程启动顺序,线程 / 进程调度等

竞争条件发生在多线程应用程序或多进程系统中。竞争条件最基本的假设是,假设不在同一线程或进程中的两件事将以特定顺序发生,而无需采取措施确保它们确实发生。这通常发生在两个线程通过设置和检查两个类都可以访问的成员变量来传递消息时。当一个线程调用 sleep 来给另一个线程完成任务的时间时,几乎总是存在竞争状态(除非 sleep 处于循环中,并且具有某种检查机制)。

防止争用情况的工具取决于语言和操作系统,但是一些常见的工具是互斥体,关键部分和信号。当您想确保自己是唯一做某事的人时,互斥体是很好的选择。当您想确保别人完成某件事时,信号是好的。最小化共享资源也可以帮助防止意外行为

检测比赛条件可能很困难,但是有一些迹象。严重依赖睡眠的代码容易出现竞争状况,因此请首先在受影响的代码中检查对睡眠的调用。添加特别长的睡眠也可以用于调试,以强制执行特定顺序的事件。这对于重现行为,查看是否可以通过更改事物的时间使其消失,以及测试已部署的解决方案很有用。调试后应删除睡眠。

但是,如果某个问题仅在某些计算机上间歇性地出现,则表明它具有竞态条件。常见的错误是崩溃和死锁。使用日志记录,您应该能够找到受影响的区域并从那里进行工作。

竞争条件是并发编程中的一种情况,其中两个并发线程或进程争用资源,并且最终状态取决于谁先获取资源。

微软实际上已经发布了有关种族条件和僵局问题的非常详细的文章 。其中最概括的摘要是标题段落:

当两个线程同时访问一个共享变量时,就会发生竞争状态。第一个线程读取变量,第二个线程从变量读取相同的值。然后,第一个线程和第二个线程对值执行操作,然后争先看哪个线程可以最后将值写入共享变量。保留最后写入其值的线程的值,因为该线程正在覆盖前一个线程写入的值。