在编写多线程应用程序时,最常见的问题之一是竞态条件。
我对社区的问题是:
- 什么是比赛条件?
- 你如何检测它们?
- 你如何处理它们?
- 最后,如何防止它们发生?
在编写多线程应用程序时,最常见的问题之一是竞态条件。
我对社区的问题是:
当两个或多个线程可以访问共享数据并且它们尝试同时更改它时,就会出现竞争条件。因为线程调度算法可以随时在线程之间交换,所以你不知道线程尝试访问共享数据的顺序。因此,数据更改的结果取决于线程调度算法,即两个线程都在“竞相”访问/更改数据。
当一个线程执行“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 的值最终不会是 50,000,000。事实上,它会随着每次运行而变化。
这是因为,为了让每个线程增加 x 的值,它们必须执行以下操作:(显然是简化的)
检索 x 的值 将此值加 1 将此值存储到 x
任何线程在任何时候都可以处于这个过程中的任何一步,当涉及到共享资源时,它们可以互相踩踏。在读取 x 和写回 x 之间的时间期间,另一个线程可以更改 x 的状态。
假设一个线程检索了 x 的值,但还没有存储它。另一个线程也可以检索相同的 x 值(因为还没有线程更改它),然后它们都将相同的值 (x+1) 存储回 x!
例子:
线程1:读取x,值为7 线程 1:将 1 加到 x,现在值为 8 线程 2:读取 x,值为 7 线程 1:在 x 中存储 8 线程 2:将 1 加到 x,现在值为 8 线程 2:在 x 中存储 8
可以通过在访问共享资源的代码之前采用某种锁定机制来避免竞争条件:
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、并发数据结构、适当的同步和基于参与者的并发将有所帮助。
最好的并发资源是 JCIP。您还可以在此处获得有关上述说明的更多详细信息。
竞争条件和数据竞争之间存在重要的技术差异。大多数答案似乎都假设这些术语是等价的,但事实并非如此。
当 2 条指令访问相同的内存位置时会发生数据竞争,这些访问中至少有一个是写入,并且在这些访问之间排序之前没有发生任何事情。现在,什么构成了在排序之前发生的事情有很多争论,但一般来说,同一锁变量上的 ulock-lock 对和同一条件变量上的等待信号对会导致发生前发生的顺序。
竞争条件是语义错误。它是发生在时间或事件顺序中的缺陷,会导致错误的程序行为。
许多竞争条件可能(实际上是)由数据竞争引起,但这不是必需的。事实上,数据竞争和竞争条件既不是彼此的必要条件,也不是充分条件。这篇博文也很好地解释了差异,并以一个简单的银行交易示例为例。这是另一个解释差异的简单示例。
现在我们确定了术语,让我们尝试回答最初的问题。
鉴于竞争条件是语义错误,因此没有检测它们的通用方法。这是因为在一般情况下,没有办法拥有可以区分正确和不正确程序行为的自动预言机。种族检测是一个不可判定的问题。
另一方面,数据竞争有一个精确的定义,不一定与正确性相关,因此可以检测到它们。有多种数据竞争检测器(静态/动态数据竞争检测、基于锁集的数据竞争检测、基于发生前的数据竞争检测、混合数据竞争检测)。最先进的动态数据竞争检测器是ThreadSanitizer,它在实践中运行良好。
处理数据竞争通常需要一些编程规则来诱导访问共享数据之间的先发生边缘(在开发期间,或者一旦使用上述工具检测到它们)。这可以通过锁、条件变量、信号量等来完成。但是,也可以采用不同的编程范式,如消息传递(而不是共享内存),通过构造来避免数据竞争。
一种规范的定义是“当两个线程同时访问内存中的同一位置,并且至少其中一个访问是写入时”。在这种情况下,“读者”线程可能会获得旧值或新值,这取决于哪个线程“赢得比赛”。这并不总是一个错误——事实上,一些非常复杂的低级算法是故意这样做的——但通常应该避免它。@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 获得处理器时,它将执行“除以零”。
此错误仅在线程 A 在 if 语句之后被抢占时才会发生,这种情况非常罕见,但它可能会发生。
竞争条件是并发编程中的一种情况,其中两个并发线程或进程竞争资源,最终状态取决于谁首先获得资源。
竞态条件不仅与软件有关,也与硬件有关。实际上,该术语最初是由硬件行业创造的。
根据维基百科:
该术语起源于两个信号相互竞争以 首先影响输出的想法。
逻辑电路中的竞争条件:
软件行业不加修改就取了这个词,有点难理解。
您需要进行一些替换以将其映射到软件世界:
所以软件行业中的竞争条件是指“两个线程”/“两个进程”相互竞争以“影响某些共享状态”,而共享状态的最终结果将取决于一些细微的时间差异,这可能是由某些特定的线程/进程启动顺序、线程/进程调度等
竞争条件发生在多线程应用程序或多进程系统中。最基本的竞争条件是任何假设不在同一个线程或进程中的两件事将按特定顺序发生,而不采取措施确保它们发生的任何事情。当两个线程通过设置和检查一个类的成员变量都可以访问来传递消息时,通常会发生这种情况。当一个线程调用 sleep 以给另一个线程时间来完成任务时,几乎总是存在竞争条件(除非 sleep 处于循环中,并带有一些检查机制)。
防止竞争条件的工具取决于语言和操作系统,但一些常见的工具是互斥锁、临界区和信号。当你想确保你是唯一一个做某事的人时,互斥锁是很好的。当你想确保别人已经完成某件事时,信号是好的。最小化共享资源也有助于防止意外行为
检测竞争条件可能很困难,但有几个迹象。严重依赖睡眠的代码容易出现竞争条件,因此首先检查受影响代码中的睡眠调用。添加特别长的睡眠也可用于调试以尝试强制执行特定的事件顺序。这对于重现行为、查看是否可以通过更改事物的时间来使其消失以及测试已实施的解决方案很有用。调试后应删除睡眠。
但是,如果存在仅在某些机器上间歇性发生的问题,则表明存在竞争条件的签名标志。常见的错误是崩溃和死锁。通过日志记录,您应该能够找到受影响的区域并从那里恢复工作。
微软实际上已经发表了一篇关于竞争条件和死锁问题的非常详细的文章。其中最总结的摘要将是标题段落:
当两个线程同时访问一个共享变量时,就会出现竞争条件。第一个线程读取变量,第二个线程从变量中读取相同的值。然后第一个线程和第二个线程对该值执行它们的操作,并竞相查看哪个线程可以最后将值写入共享变量。最后写入其值的线程的值被保留,因为该线程正在覆盖前一个线程写入的值。
什么是竞态条件?
过程严重依赖于其他事件的顺序或时间的情况。
例如,处理器 A 和处理器 B都需要相同的资源来执行它们。
你如何检测它们?
有一些工具可以自动检测竞态条件:
你如何处理它们?
竞争条件可以由Mutex或Semaphores处理。它们充当锁,允许进程根据某些要求获取资源以防止竞争条件。
你如何防止它们发生?
有多种方法可以防止竞争条件,例如避免关键部分。
竞争条件是当设备或系统尝试同时执行两个或多个操作时发生的不希望的情况,但由于设备或系统的性质,操作必须以正确的顺序完成,以便正确完成。
在计算机内存或存储中,如果几乎在同一时刻接收到读取和写入大量数据的命令,并且机器尝试覆盖部分或全部旧数据,而旧数据仍在执行中,则可能会出现竞争条件。读。结果可能是以下一种或多种:计算机崩溃、“非法操作”、程序的通知和关闭、读取旧数据的错误或写入新数据的错误。
如果您使用“原子”类,您可以防止竞争条件。原因只是线程没有分开操作get和set,示例如下:
AtomicInteger ai = new AtomicInteger(2);
ai.getAndAdd(5);
结果,您将在链接“ai”中有 7 个。虽然你做了两个动作,但是这两个操作都确认了同一个线程并且没有其他线程会干扰这个,这意味着没有竞争条件!
这是经典的银行账户余额示例,它将帮助新手在竞争条件下轻松理解 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));
}
尝试这个基本示例以更好地理解竞争条件:
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;
}
}
}
您并不总是想放弃竞争条件。如果您有一个可以由多个线程读取和写入的标志,并且该标志由一个线程设置为“完成”,以便在标志设置为“完成”时其他线程停止处理,那么您不希望“竞争”条件”予以消除。实际上,这可以称为良性竞争条件。
但是,使用检测竞态条件的工具,会发现它是有害的竞态条件。
有关竞争条件的更多详细信息,请访问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显示之前更新值。这里存在一个竞争条件。竞争条件可以通过使用同步来解决
竞争条件是当两个或多个进程可以同时访问和更改共享数据时发生的不良情况。它发生是因为对资源的访问存在冲突。临界区问题可能会导致竞态条件。为了解决进程中的临界条件,我们一次只取出一个执行临界区的进程。