编写多线程应用程序时,遇到的最常见问题之一是竞争条件.
我对社区的问题是:
什么是比赛条件?你怎么发现它们?你怎么处理它们?最后,你如何防止它们发生?
当两个或多个线程可以访问共享数据并且他们尝试同时更改它时,会发生竞争条件.因为线程调度算法可以在任何时间在线程之间交换,所以您不知道线程将尝试访问共享数据的顺序.因此,数据变化的结果取决于线程调度算法,即两个线程都"竞相"访问/改变数据.
当一个线程执行"check-then-act"时(例如,"检查",如果值为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
当访问共享资源的多线程(或其他并行)代码可能以导致意外结果的方式执行时,存在"竞争条件".
举个例子:
for ( int i = 0; i < 10000000; i++ )
{
x = x + 1;
}
如果您有5个线程同时执行此代码,则x WOULD NOT的值最终为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 + 1)存储回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
}
在这里,答案每次都是50,000,000.
有关锁定的更多信息,请搜索:互斥锁,信号量,临界区,共享资源.
什么是比赛条件?
你计划在下午5点去看电影.您可以在下午4点查询门票的可用性.该代表说他们可以使用.您可以在演出前5分钟放松并到达售票窗口.我相信你可以猜到会发生什么:这是一个完整的房子.这里的问题是检查和行动之间的持续时间.你在4点询问并在5点采取行动.与此同时,其他人抓住了门票.这是一种竞争条件 - 特别是竞争条件下的"检查然后行动"情景.
你怎么发现它们?
宗教代码审查,多线程单元测试.没有捷径.这个Eclipse插件很少出现,但还没有稳定.
你如何处理和预防他们?
最好的方法是创建无副作用和无状态函数,尽可能使用不可变的函数.但这并非总是可行的.因此,使用java.util.concurrent.atomic,并发数据结构,正确的同步和基于actor的并发性将有所帮助.
最佳的并发资源是JCIP.您还可以在此处获得有关上述说明的更多详细信息.
种族条件和数据竞赛之间存在重要的技术差异.大多数答案似乎都假设这些术语是等价的,但事实并非如此.
当2个指令访问相同的存储器位置时发生数据竞争,这些访问中的至少一个是写入,并且在这些访问中进行排序之前没有发生.现在,在排序之前构成一个事件的问题会受到很多争论,但一般来说,同一个锁变量上的ulock-lock对和同一个条件变量上的等待信号对会导致一个先发生的顺序.
竞争条件是语义错误.这是导致错误的程序行为的事件的时间或顺序中发生的缺陷.
许多竞争条件可能(实际上是)由数据竞争引起,但这不是必需的.事实上,数据竞赛和竞争条件既不是必要的,也不是彼此的充分条件.这篇博文还通过简单的银行交易示例很好地解释了这种差异.这是另一个解释差异的简单例子.
现在我们确定了术语,让我们尝试回答原始问题.
鉴于竞争条件是语义错误,没有检测它们的一般方法.这是因为在一般情况下无法使用能够区分正确与不正确程序行为的自动化oracle.种族检测是一个不可判定的问题.
另一方面,数据竞赛具有精确的定义,不一定与正确性相关,因此可以检测它们.有许多种类的数据竞争检测器(静态/动态数据竞争检测,基于锁定的数据竞争检测,基于发生的数据竞争检测,混合数据竞争检测).最先进的动态数据竞争检测器是ThreadSanitizer,它在实践中运行良好.
处理数据竞争通常需要一些编程规则来诱导在共享数据访问之前发生 - 在开发期间或使用上述工具检测到它们之后.这可以通过锁,条件变量,信号量等来完成.但是,也可以采用不同的编程范例,如消息传递(而不是共享内存),以避免构造中的数据争用.
一种规范的定义是" 当两个线程同时访问内存中的相同位置时,并且至少一个访问是写入." 在这种情况下,"reader"线程可能会获得旧值或新值,具体取决于哪个线程"赢得比赛".这并不总是一个bug - 事实上,一些真正有毛的低级算法是故意这样做的 - 但通常应该避免这种情况.@Steve Gury给出了一个很好的例子,它可能是一个问题.
竞争条件是一种只在某些时间条件下发生的错误.
示例:想象一下,您有两个线程,A和B.
在线程A中:
if( object.a != 0 )
object.avg = total / object.a
在线程B中:
object.a = 0
如果线程A在检查到object.a不为空之后被抢占,则B将执行a = 0
,并且当线程A将获得处理器时,它将执行"除以零".
只有当线程A在if语句之后被抢占时才会发生此错误,这种情况非常罕见,但它可能会发生.
竞争条件不仅与软件有关,而且与硬件有关.实际上这个术语最初是由硬件行业创造的.
根据维基百科:
该术语起源于两个信号相互竞争以 首先影响输出的想法.
逻辑电路中的竞争条件:
理解这个术语的软件行业没有修改,这使得它有点难以理解.
您需要进行一些替换以将其映射到软件世界:
"两个信号"=>"两个线程"/"两个进程"
"影响输出"=>"影响某些共享状态"
因此,软件行业的竞争条件意味着"两个线程"/"两个进程"相互竞争"影响某些共享状态",共享状态的最终结果将取决于一些微妙的时序差异,这可能是由某些特定的线程/进程启动顺序,线程/进程调度等
竞争条件发生在多线程应用程序或多进程系统中.最基本的竞争条件是假设两个不在同一个线程或过程中的事物将按特定顺序发生,而不采取措施确保它们这样做.当两个线程通过设置和检查类都可以访问的成员变量来传递消息时,通常会发生这种情况.当一个线程调用sleep以给另一个线程时间来完成一个任务时,几乎总是存在竞争条件(除非该休眠处于循环中,具有一些检查机制).
用于防止竞争条件的工具取决于语言和操作系统,但是一些常见的工具是互斥锁,关键部分和信号.当你想要确保你是唯一一个做某事的人时,互斥体是好的.当你想确保其他人已经完成某些事情时,信号很好.最小化共享资源还可以帮助防止意外行为
检测竞争条件可能很困难,但有几个迹象.严重依赖睡眠的代码容易出现竞争条件,因此请首先检查受影响代码中的睡眠呼叫.添加特别长的睡眠也可以用于调试以尝试强制特定的事件顺序.这可以用于重现行为,看看是否可以通过改变事物的时间使其消失,以及测试解决方案.调试后应该删除睡眠.
但是,如果存在仅在某些机器上间歇性地发生的问题,那么签名符号就是竞争条件.常见的错误是崩溃和死锁.通过日志记录,您应该能够找到受影响的区域并从那里开始工作.
竞争条件是并发编程的情况,其中两个并发线程或进程竞争资源,结果最终状态取决于谁首先获取资源.
微软实际上发布了一篇关于竞争条件和死锁问题的非常详细的文章.其中摘要最多的摘要是标题段落:
当两个线程同时访问共享变量时,会发生竞争条件.第一个线程读取变量,第二个线程从变量读取相同的值.然后第一个线程和第二个线程对值执行操作,然后它们竞争以查看哪个线程可以将值最后写入共享变量.保留最后写入其值的线程的值,因为该线程正在写入前一个线程写入的值.
什么是比赛条件?
该过程严重依赖于其他事件的顺序或时间的情况。
例如,处理器A和处理器B 都需要相同的资源来执行。
您如何检测到它们?
有一些工具可以自动检测比赛状况:
基于锁集的种族检查器
发生种族检测之前
混合种族检测
您如何处理它们?
种族条件可以通过Mutex或Semaphores处理。它们充当锁,使进程可以根据某些要求来获取资源,以防止出现竞争状况。
您如何防止它们发生?
有多种防止竞争状况的方法,例如“避免临界区”。
在其关键区域内没有两个进程同时进行。(互斥)
没有关于速度或CPU数量的假设。
没有任何进程在其关键区域之外运行会阻塞其他进程。
无需过程就永远等待进入其关键区域。(A等待B资源,B等待C资源,C等待A资源)