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

我对社区的问题是:

竞态条件是什么? 你如何发现它们? 你是如何处理的? 最后,你如何防止它们的发生?


当前回答

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

其他回答

竞态条件是一种bug,只会在特定的时间条件下发生。

例子: 假设您有两个线程,A和B。

在线程A中:

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

线程B:

object.a = 0

如果线程A在检查完对象后被抢占。a不为空,B将执行a = 0,当线程a将获得处理器时,它将执行“除零”。

这个错误只发生在if语句之后的线程A被抢占时,这是非常罕见的,但它是有可能发生的。

当两个或多个线程可以访问共享数据,并且它们试图同时更改数据时,就会发生竞态条件。因为线程调度算法可以在任何时候在线程之间交换,所以您不知道线程将尝试访问共享数据的顺序。因此,数据更改的结果依赖于线程调度算法,即两个线程都在“竞相”访问/更改数据。

当一个线程执行“检查-然后-行动”时,问题经常发生。“check”如果值是X,那么“act”做一些取决于值是X的事情),另一个线程在“check”和“act”之间对值做一些事情。例句:

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.
}

这一点是,y可以是10,也可以是任何值,这取决于在检查和执行之间是否有另一个线程改变了x。你根本不知道。

为了防止竞争条件的发生,您通常会在共享数据周围放置一个锁,以确保一次只有一个线程可以访问数据。这意味着:

// 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

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

当两条指令访问相同的内存位置时,就会发生数据竞争,其中至少有一个是写,并且在这些访问之间没有发生排序之前。现在,什么构成happens before顺序还存在很多争论,但一般来说,同一个锁变量上的ulock-lock对和同一个条件变量上的wait-signal对诱导的是happens before顺序。

竞态条件是语义错误。它是发生在事件的时间或顺序上的缺陷,会导致错误的程序行为。

许多竞态条件可能(事实上也是)是由数据竞态引起的,但这是不必要的。事实上,数据竞争和竞争条件既不是彼此的必要条件,也不是彼此的充分条件。这篇博客文章也用一个简单的银行交易例子很好地解释了两者的区别。下面是另一个简单的例子来解释这种区别。

既然我们已经确定了术语,让我们试着回答最初的问题。

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

另一方面,数据竞争有一个精确的定义,它不一定与正确性有关,因此可以检测到它们。数据竞争检测器有很多种(静态/动态数据竞争检测、基于锁集的数据竞争检测、基于先于事件发生的数据竞争检测、混合数据竞争检测)。最先进的动态数据竞争检测器是ThreadSanitizer,它在实践中工作得非常好。

处理数据竞争通常需要一些编程规程来在访问共享数据之间的边之前诱导happens(在开发过程中,或者在使用上述工具检测到它们之后)。这可以通过锁、条件变量、信号量等来实现。但是,还可以使用不同的编程范例,例如消息传递(而不是共享内存),以避免构造造成的数据竞争。

如果你使用“原子”类,你可以防止竞争条件。原因是线程没有分开操作get和set,示例如下:

AtomicInteger ai = new AtomicInteger(2);
ai.getAndAdd(5);

因此,你将有7在链接“ai”。 虽然你做了两个操作,但这两个操作都确认了同一个线程,没有其他线程会干涉这,这意味着没有竞争条件!

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