什么是比赛条件?

时间:2020-03-05 18:45:00  来源:igfitidea点击:

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

我对社区的问题是:

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

解决方案

回答

竞争条件是一种错误,仅在某些时间条件下才会发生。

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

在线程A中:

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

在线程B中:

object.a = 0

如果线程A在检查完object.a不为null之后立即被抢占,则B将执行'a = 0',并且当线程A将获得处理器时,它将执行"被零除"。

仅当在if语句之后抢占线程A时,才会发生此错误,这种情况很少见,但是有可能发生。

回答

当两个或者多个线程可以访问共享数据并且它们试图同时更改它们时,就会发生争用情况。因为线程调度算法可以随时在线程之间交换,所以我们不知道线程尝试访问共享数据的顺序。因此,数据改变的结果取决于线程调度算法,即,两个线程都在"竞争"以访问/改变数据。

当一个线程执行"先检查后执行"操作(例如,如果值是X时进行"检查",然后执行"操作"以执行取决于该值为X的操作),而另一个线程对其中的值进行操作时,通常会出现问题。在"检查"和"操作"之间。例如:

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

回答

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

回答

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

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

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

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

回答

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

回答

当将访问共享资源的多线程(或者并行)代码以某种方式引起意外结果时,就会出现"竞争条件"。

举个例子:

for ( int i = 0; i < 10000000; i++ )
{
   x = x + 1; 
}

如果我们有5个线程一次执行此代码,则x的值最终将不会是50,000,000。实际上,每次运行都会有所不同。

这是因为,为了使每个线程增加x的值,它们必须执行以下操作:(显然,是简化的)

Retrieve the value of x
Add 1 to this value
Store this value to x

任何线程都可以随时处于此过程中的任何步骤,并且当涉及共享资源时,它们可以彼此并进。在读取x到写入x之间的时间内,另一个线程可以更改x的状态。

假设某个线程检索x的值,但尚未存储它。另一个线程也可以检索x的相同值(因为尚未有任何线程对其进行更改),然后它们都将在x中存储相同的值(x + 1)!

例子:

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
}

在这里,答案每次都是5000万。

有关锁定的更多信息,请搜索:互斥锁,信号量,关键部分,共享资源。