什么是比赛条件?

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

我对社区的问题是:

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

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

当一个线程做一个“check-then-act”(例如“check”,如果这个值是X,然后“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,也可能是任何东西,这取决于另一个线程在check和act之间是否改变了x。 你没有真正的知道。

为了防止竞争状况的发生,通常会对共享数据进行locking,以确保一次只有一个线程可以访问数据。 这将意味着这样的事情:

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

当访问共享资源的multithreading(或其他并行)代码可能导致意想不到的结果时,会出现“竞态条件”。

以这个例子:

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

如果你有5个线程一次执行这个代码,x的值不会最终为50,000,000。 事实上,每次运行都会有所不同。

这是因为,为了使每个线程增加x的值,他们必须做到以下几点:(简化,显然)

检索x的值
给这个值加1
将此值存储到x

任何线程在任何时候都可以在这个进程中的任何一个步骤中,并且当涉及共享资源时它们可以相互进行。 在x被读取和写回之间,x的状态可以被另一个线程改变。

假设一个线程检索x的值,但还没有存储它。 另一个线程也可以检索相同的x值(因为没有线程已经改变了它),然后他们都将相同的值(x + 1)存回x!

例:

线程1:读取x,值是7
线程1:将x加1,现在值为8
线程2:读取x, 值是7
主题1:存储8个x
线程2:将x加1,现在值为8
线程2: 存储8个x

竞争条件可以通过在访问共享资源的代码之前使用某种locking机制来避免:

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

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

有关locking的更多信息,请search:互斥体,信号量,临界区,共享资源。

什么是种族条件?

你打算在下午五点去看电影。 你在下午4点询问门票的可用性。 该代表说,他们可用。 在放映前5分钟放松并到达售票窗口。 我相信你可以猜到会发生什么事情:这是一个完整的房子。 问题在于检查和行动之间的时间。 你在4点询问,并在5点行事。同时,别人抢了票。 这是一个竞赛条件 – 特别是一个“检查 – 然后行动”的竞争条件的情况。

你如何检测他们?

宗教代码审查,multithreadingunit testing。 没有捷径。 有几个Eclipse插件正在出现,但没有稳定。

你如何处理和预防他们?

最好的做法是创build副作用的自由和无状态的function,尽可能地使用不可变的。 但是这并不总是可能的。 因此,使用java.util.concurrent.atomic,并发数据结构,适当的同步和基于angular色的并发将会有所帮助。

最好的并发资源是JCIP。 你也可以在这里获得更多关于上述解释的细节 。

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

当两条指令访问相同的内存位置时,会发生数据竞争,至less其中一个访问是写入操作,并且在这些访问之间sorting之前不会发生 。 现在在订购之前发生了什么事情需要进行大量的争论,但总的来说,相同条件variables上的lockingvariables和等待信号对上的ulock-lock对会在订单之前产生一个事件。

竞赛条件是一个语义错误。 这是发生在导致错误程序行为的事件的时间或顺序上的缺陷。

许多竞赛条件可能(事实上)是由数据竞赛引起的,但这不是必要的。 事实上,数据竞赛和竞赛条件既不是必要的,也不是充分的条件。 这个博客文章也很好地解释了这个差别,用一个简单的银行交易的例子。 这是另一个解释差异的简单例子 。

现在我们确定了术语,让我们试着回答原来的问题。

鉴于竞争条件是语义错误,检测它们没有一般的方法。 这是因为在一般情况下,没有办法可以区分正确的程序行为和不正确的程序行为。 种族检测是一个不可判定的问题。

另一方面,数据竞赛有一个精确的定义,不一定与正确性有关,因此可以检测它们。 有许多种类的数据竞争检测器(静态/dynamic数据竞争检测,基于locking的数据竞争检测,发生之前基于数据竞争检测,混合数据竞争检测)。 最先进的dynamic数据竞赛探测器是ThreadSanitizer ,在实践中运行良好。

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

一种规范的定义是“ 当两个线程同时访问内存中的同一个位置,并且至less有一个访问是写入 。 在这种情况下,“读者”线程可能会获得旧值或新值,这取决于哪个线程“赢得了比赛”。 这并不总是一个错误 – 事实上,一些真正有毛病的低级algorithm是故意的 – 但通常应该避免。 @Steve Gury给出了一个很好的例子,它可能是一个问题。

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

例子:假设你有两个线程,A和B.

在线程A:

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

在线程B中:

 object.a = 0 

如果线程A在检查到object.a不是null之后被抢先,那么B将执行a = 0 ,并且当线程A获得处理器时,它将执行“除以零”。

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

竞争情况发生在multithreading应用程序或多进程系统中。 最基本的竞争条件是任何可以假设两件事情不在同一个线程或过程中的事情会按照特定的顺序发生,而没有采取措施来确保它们。 这种情况通常发生在两个线程通过设置和检查一个类的成员variables都可以访问来传递消息时。 当一个线程调用睡眠来给出另一个线程时间来完成一个任务(除非该睡眠处于一个循环中,并带有一些检查机制),几乎总是有一个竞争条件。

防止竞争条件的工具取决于语言和操作系统,但是一些como的是互斥体,关键部分和信号。 互斥体是好的,当你想确保你是唯一做某事的人。 信号是好的,当你想确定别人已经完成了一些事情。 最小化共享资源还可以帮助防止意外的行为

检测竞赛状况可能很困难,但有一些迹象。 严重依赖睡眠的代码很容易出现竞争状况,所以首先检查受影响代码中是否需要睡眠。 添加特别长的睡眠也可以用于debugging,以尝试强制事件的特定顺序。 这对再现行为非常有用,看看是否可以通过改变事物的时间使其消失,并testing解决scheme。 debugging后应该删除睡眠。

不过,签名标志说明有一个竞争条件,如果有一个问题只是在某些机器上间歇性地发生。 常见的错误将是崩溃和死锁。 通过日志logging,您应该能够find受影响的区域并从那里恢复工作。

微软实际上已经发表了关于这个竞争条件和僵局问题的非常详细的文章 。 摘要中摘要最多的是标题段落:

当两个线程同时访问一个共享variables时,会发生争用情况。 第一个线程读取variables,第二个线程从variables读取相同的值。 然后第一个线程和第二个线程对这个值执行它们的操作,并且它们竞相查看哪个线程可以将最后一个值写入共享variables。 最后写入其值的线程的值将被保留,因为线程正在写入上一个线程写入的值。

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

竞态条件是当设备或系统同时尝试执行两个或更多个操作时发生的不希望的情况,但是由于设备或系统的性质,操作必须以正确的顺序进行以成为正确完成。

在计算机存储器或存储器中,如果在几乎同一时刻接收到读取和写入大量数据的命令,则可能发生争用情况,并且机器尝试覆盖部分或全部旧数据,而旧数据仍然存在读。 其结果可能是以下一项或多项:计算机崩溃,“非法操作”,程序通知和closures,读取旧数据时出错,或写入新数据时出错。

这里是经典的银行帐户余额的例子,这将帮助新手轻松了解竞争条件的Java中的线程:

 public class BankAccount { /** * @param args */ int accountNumber; double accountBalance; public synchronized boolean Deposit(double amount){ double newAccountBalance=0; if(amount<=0){ return false; } else { newAccountBalance = accountBalance+amount; accountBalance=newAccountBalance; return true; } } public synchronized boolean Withdraw(double amount){ double newAccountBalance=0; if(amount>accountBalance){ return false; } else{ newAccountBalance = accountBalance-amount; accountBalance=newAccountBalance; return true; } } public static void main(String[] args) { // TODO Auto-generated method stub BankAccount b = new BankAccount(); b.accountBalance=2000; System.out.println(b.Withdraw(3000)); } 

比赛条件不仅与软件相关,而且与硬件相关。 其实这个词最初是由硬件行业创造的。

根据维基百科 :

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

逻辑电路中的竞态条件:

在这里输入图像描述

软件行业没有修改就这个词,这使得它有点难以理解。

您需要做一些replace,将其映射到软件世界:

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

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

好的,这4个问题。 一个一个的答案就如同…

什么是比赛条件?

当过程的输出和/或结果严重依赖于其他事件的顺序或时间时,即,例如2个信号竞相改变输出。

你如何检测他们?

这导致了难以定位的错误。

你如何处理它们?

使用信号量

最后,

你如何防止它们发生?

避免竞争条件的一种方法是使用资源locking机制。 但locking资源可能会导致死锁。 必须处理。

尝试这个基本的例子,以更好地了解竞争条件:

  public class ThreadRaceCondition { /** * @param args * @throws InterruptedException */ public static void main(String[] args) throws InterruptedException { Account myAccount = new Account(22222222); // Expected deposit: 250 for (int i = 0; i < 50; i++) { Transaction t = new Transaction(myAccount, Transaction.TransactionType.DEPOSIT, 5.00); t.start(); } // Expected withdrawal: 50 for (int i = 0; i < 50; i++) { Transaction t = new Transaction(myAccount, Transaction.TransactionType.WITHDRAW, 1.00); t.start(); } // Temporary sleep to ensure all threads are completed. Don't use in // realworld :-) Thread.sleep(1000); // Expected account balance is 200 System.out.println("Final Account Balance: " + myAccount.getAccountBalance()); } } class Transaction extends Thread { public static enum TransactionType { DEPOSIT(1), WITHDRAW(2); private int value; private TransactionType(int value) { this.value = value; } public int getValue() { return value; } }; private TransactionType transactionType; private Account account; private double amount; /* * If transactionType == 1, deposit else if transactionType == 2 withdraw */ public Transaction(Account account, TransactionType transactionType, double amount) { this.transactionType = transactionType; this.account = account; this.amount = amount; } public void run() { switch (this.transactionType) { case DEPOSIT: deposit(); printBalance(); break; case WITHDRAW: withdraw(); printBalance(); break; default: System.out.println("NOT A VALID TRANSACTION"); } ; } public void deposit() { this.account.deposit(this.amount); } public void withdraw() { this.account.withdraw(amount); } public void printBalance() { System.out.println(Thread.currentThread().getName() + " : TransactionType: " + this.transactionType + ", Amount: " + this.amount); System.out.println("Account Balance: " + this.account.getAccountBalance()); } } class Account { private int accountNumber; private double accountBalance; public int getAccountNumber() { return accountNumber; } public double getAccountBalance() { return accountBalance; } public Account(int accountNumber) { this.accountNumber = accountNumber; } // If this method is not synchronized, you will see race condition on // Remove syncronized keyword to see race condition public synchronized boolean deposit(double amount) { if (amount < 0) { return false; } else { accountBalance = accountBalance + amount; return true; } } // If this method is not synchronized, you will see race condition on // Remove syncronized keyword to see race condition public synchronized boolean withdraw(double amount) { if (amount > accountBalance) { return false; } else { accountBalance = accountBalance - amount; return true; } } } 

你并不总是想放弃竞赛条件。 如果你有一个可以被多个线程读写的标志,并且这个标志被一个线程设置为'done',以便当标志被设置为'done'时其他线程停止处理,你不希望这个“race条件“被淘汰。 其实这个可以说是一个良性的竞赛条件。

但是,使用一个工具来检测竞争条件,它将被视为有害的竞争条件。

有关比赛条件的更多细节,请访问http://msdn.microsoft.com/en-us/magazine/cc546569.aspx

考虑一个操作,只要计数递增就必须显示计数。 即,一旦CounterThread递增, DisplayThread需要显示最近更新的值。

 int i = 0; 

产量

 CounterThread -> i = 1 DisplayThread -> i = 1 CounterThread -> i = 2 CounterThread -> i = 3 CounterThread -> i = 4 DisplayThread -> i = 4 

这里CounterThread经常获取锁,并在DisplayThread显示之前更新该值。 这里存在一个竞争条件。 比赛条件可以通过使用同步解决

如果你使用“primefaces”类,你可以防止竞争条件 。 原因就是线程不分离操作得到并设置,例如:

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

因此,你将有7在链接“爱”。 虽然你做了两个动作,但是两个操作都确认了同一个线程,没有其他的线程会干扰到这个,那就意味着没有竞赛条件!