什么是种族条件?

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

我对社区的问题是:

  • 比赛条件是什么?
  • 你如何检测它们?
  • 你怎么对付他们?
  • 最后,如何防止它们发生?
817236 次浏览

竞争条件是一种bug,只有在一定的时间条件下才会发生。

示例:假设你有两个线程,A和B。

在线程A中:

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

在线程B中:

object.a = 0

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

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

当两个或多个线程可以访问共享数据并且它们试图同时更改时,就会出现竞争条件。因为线程调度算法可以随时在线程之间交换,你不知道线程将尝试访问共享数据的顺序。因此,数据变化的结果取决于线程调度算法,即两个线程都“竞相”访问/更改数据。

当一个线程执行“check-then-act”(例如,如果值是X,则“check”,然后“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 xif (x == 5){y = x * 2; // Now, nothing can change x until the lock is released.// Therefore y = 10}// release lock for x

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

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

防止竞争条件的工具取决于语言和操作系统,但一些常见的工具是互斥锁、临界区和信号。当你想确保只有你一个人在做某事时,互斥锁很好。当你想确保其他人已经完成某事时,信号也很好。最小化共享资源还有助于防止意外行为

检测竞争条件可能很困难,但有几个迹象。严重依赖休眠的代码容易出现竞争条件,因此首先检查受影响代码中是否有调用休眠。添加特别长的休眠也可用于调试,尝试强制执行特定的事件顺序。这对于再现行为、查看是否可以通过更改时间让它消失以及测试到位的解决方案很有用。调试后应该删除休眠。

但是,存在竞争条件的标志是,如果存在仅在某些机器上间歇性发生的问题。常见的错误是崩溃和死锁。使用日志记录,您应该能够找到受影响的区域并从那里返回。

一种规范的定义是“当两个线程同时访问内存中的同一位置,并且至少有一个访问是写”。在这种情况下,“阅读器”线程可能会获得旧值或新值,这取决于哪个线程“赢得比赛”。这并不总是一个bug-事实上,一些非常毛茸茸的低级算法故意这样做-但通常应该避免。

当访问共享资源的多线程(或以其他方式并行)代码可能以导致意外结果的方式这样做时,存在“竞争条件”。

举个例子:

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

如果您有5个线程同时执行此代码,x的值最终不会是50,000,000。它实际上会随着每次运行而变化。

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

Retrieve the value of xAdd 1 to this valueStore this value to x

任何线程都可以随时处于该进程的任何步骤,当涉及共享资源时,它们可以相互踩踏。在读取x和写回x之间的时间内,另一个线程可以更改x的状态。

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

示例:

Thread 1: reads x, value is 7Thread 1: add 1 to x, value is now 8Thread 2: reads x, value is 7Thread 1: stores 8 in xThread 2: adds 1 to x, value is now 8Thread 2: stores 8 in x

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

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

在这里,答案每次都是50,000,000。

有关锁定的更多信息,请搜索:互斥锁、信号量、临界区、共享资源。

这是经典的银行账户余额示例,它将帮助新手轻松理解Java线程w. r. t.竞争条件:

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 stubBankAccount b = new BankAccount();b.accountBalance=2000;System.out.println(b.Withdraw(3000));
}

微软实际上已经就竞争条件和死锁问题发布了非常详细的文章。其中最总结的摘要是标题段落:

当两个线程访问共享变量时,会出现竞争条件同时。第一个线程读取变量,第二个线程读取变量线程从变量中读取相同的值。然后第一个线程和第二个线程对值执行操作,然后它们竞争查看哪个线程可以将值最后写入共享变量。最后写入其值的线程的值被保留,因为线程正在写入前一个线程的值写的

尝试以下基本示例以更好地理解竞赛条件:

    public class ThreadRaceCondition {
/*** @param args* @throws InterruptedException*/public static void main(String[] args) throws InterruptedException {Account myAccount = new Account(22222222);
// Expected deposit: 250for (int i = 0; i < 50; i++) {Transaction t = new Transaction(myAccount,Transaction.TransactionType.DEPOSIT, 5.00);t.start();}
// Expected withdrawal: 50for (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 200System.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 conditionpublic 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 conditionpublic synchronized boolean withdraw(double amount) {if (amount > accountBalance) {return false;} else {accountBalance = accountBalance - amount;return true;}}}

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

当2条指令访问相同的内存位置时,就会发生数据竞争,这些访问中至少有一次是写入,并且这些访问中没有在订购之前发生。现在,什么构成了一个发生在排序之前是有很多争论的,但一般来说,同一个锁变量上的ulock-lock对和同一个条件变量上的等待信号对会导致一个发生在排序之前。

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

许多竞争条件可以(并且实际上是)由数据竞争引起的,但这不是必要的。事实上,数据竞争和竞争条件既不是彼此的必要条件,也不是彼此的充分条件。

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

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

另一方面,数据竞争有一个精确的定义,不一定与正确性有关,因此可以检测到它们。数据竞争检测器有很多种风格(静态/动态数据竞争检测、基于锁集的数据竞争检测、基于发生之前的数据竞争检测、混合数据竞争检测)。最先进的动态数据竞争检测器是线程消毒剂,它在实践中效果很好。

处理数据竞争一般需要一些编程规则来诱导访问共享数据之间的发生之前边缘(无论是在开发期间,还是使用上述工具检测到它们)。这可以通过锁、条件变量、信号量等来完成。然而,也可以采用不同的编程范式,如消息传递(而不是共享内存),通过构造来避免数据竞争。

什么是种族条件?

你计划下午5点去看电影。你在下午4点询问是否有票。售票员说有票。你放松一下,在演出前5分钟到达售票窗口。我相信你能猜到发生了什么:座无虚席。这里的问题是检查和行动之间的持续时间。你在4点询问,在5点采取行动。与此同时,其他人抢走了票。这是一个竞争条件——特别是竞争条件的“检查然后行动”场景。

你如何检测它们?

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

你如何处理和预防它们?

最好的办法是创建无副作用和无状态的函数,尽可能多地使用不可变函数。但这并不总是可能的。因此使用java.util.concurrent.atomic、并发数据结构、适当的同步和基于参与者的并发会有所帮助。

并发的最佳资源是JCIP。您还可以获得更多以上解释的细节在这里

您并不总是希望丢弃竞争条件。如果您有一个可以被多个线程读写的标志,并且此标志被一个线程设置为'完成',以便其他线程在标志设置为'完成'时停止处理,您不希望消除“竞争条件”。事实上,这可以称为良性竞争条件。

但是,使用用于检测竞争条件的工具,它将被发现为有害的竞争条件。

关于比赛条件的更多细节在这里,http://msdn.microsoft.com/en-us/magazine/cc546569.aspx

什么是种族条件?

当过程严重依赖于其他事件的顺序或时间时的情况。

例如,处理器A和处理器B都需要的执行资源相同。

你如何检测它们?

有一些工具可以自动检测竞争条件:

你怎么对付他们?

争用条件可以由互斥锁信号灯处理。它们充当锁,允许进程根据特定需求获取资源以防止争用条件。

你如何防止它们发生?

有多种方法可以防止竞争条件,例如避免临界区

  1. 没有两个进程同时在它们的临界区域内。(
  2. 没有对速度或CPU数量进行假设。
  3. 没有进程在其关键区域之外运行,这会阻止其他进程。
  4. 没有进程必须永远等待才能进入其关键区域。(A等待B资源,B等待C资源,C等待A资源)

考虑一个操作,它必须在计数增加时立即显示计数。即,只要反线程增加值显示线程就需要显示最近更新的值。

int i = 0;

产出

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

这里反线程经常获取锁并在显示线程显示它之前更新值。这里存在一个竞赛条件。竞赛条件可以通过使用同步来解决

这个讨论中的许多答案解释了什么是竞争条件。我试图解释为什么这个术语在软件行业被称为race condition

为什么叫race condition

竞争条件不仅与软件有关,也与硬件有关。实际上,这个术语最初是由硬件行业创造的。

根据wikipedia

这个术语起源于两个信号相互碰撞首先影响输出

逻辑电路中的竞争条件:

输入图片描述

软件行业未经修改就使用了这个术语,这使得它有点难以理解。

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

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

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

如果您使用“原子”类,您可以防止种族条件。原因只是线程没有分离操作get和set,示例如下:

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

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

竞争条件是指两个或多个进程同时访问和更改共享数据时出现的不良情况。它的发生是因为对资源的访问冲突。临界区问题可能导致竞争条件。为了解决进程之间的临界条件,我们一次只能取出一个执行临界区的进程。

我做了一个视频来解释这一点。

本质上,它是当你有一个状态在多个线程之间共享,并且在给定状态的第一次执行完成之前,另一个执行开始,并且给定操作的新线程的初始状态是错误的,因为先前的执行尚未完成。

因为第二次执行的初始状态是错误的,结果计算也是错误的。因为最终第二次执行将用错误的结果更新最终状态。

读取共享状态

最终计算不正确

您可以在此处查看。https://youtu.be/RWRicNoWKOY