编写多线程应用程序时,遇到的最常见问题之一是竞争条件。
我对社区的问题是:
什么是比赛条件? 你怎么发现它们? 你怎么处理它们? 最后,你如何防止它们发生?
当两个或多个线程可以访问共享数据并且他们尝试同时更改它时,会发生竞争条件。因为线程调度算法可以在任何时间在线程之间交换,所以您不知道线程将尝试访问共享数据的顺序。因此,数据变化的结果取决于线程调度算法,即两个线程都"竞相"访问/改变数据。
当一个线程执行"check-then-act"时(例如,"检查",如果值为X,然后"执行"以执行取决于值为X的操作)并且另一个线程对该值执行某些操作时,通常会出现问题在"检查"和"行为"之间。例如:
1 2 3 4 5 6 7
| 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。你没有真正的认识方式。
为了防止发生竞争条件,您通常会锁定共享数据,以确保一次只能有一个线程访问数据。这意味着这样的事情:
1 2 3 4 5 6 7
| // 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 |
当访问共享资源的多线程(或其他并行)代码可能以导致意外结果的方式执行时,存在"竞争条件"。
举个例子:
1 2 3 4
| for ( int i = 0; i < 10000000; i++ )
{
x = x + 1;
} |
如果您有5个线程同时执行此代码,则x WOULD NOT的值最终为50,000,000。事实上,每次运行都会有所不同。
这是因为,为了使每个线程增加x的值,它们必须执行以下操作:(简化,显然)
1 2 3
| Retrieve the value of x
Add 1 to this value
Store this value to x |
任何线程都可以随时处于此过程的任何步骤,并且当涉及共享资源时,它们可以相互踩踏。在读取x和写回x之间的时间内,x的状态可以由另一个线程改变。
假设一个线程检索x的值,但尚未存储它。另一个线程也可以检索相同的x值(因为还没有线程改变它),然后它们都将相同的值(x + 1)存储回x!
例:
1 2 3 4 5 6
| 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 |
通过在访问共享资源的代码之前使用某种锁定机制可以避免竞争条件:
1 2 3 4 5 6
| for ( int i = 0; i < 10000000; i++ )
{
//lock x
x = x + 1;
//unlock x
} |
在这里,答案每次都是50,000,000。
有关锁定的更多信息,请搜索:互斥锁,信号量,临界区,共享资源。
What is a Race Condition?
你计划在下午5点去看电影。您可以在下午4点查询门票的可用性。该代表说他们可以使用。您可以在演出前5分钟放松并到达售票窗口。我相信你可以猜到会发生什么:这是一个完整的房子。这里的问题是检查和行动之间的持续时间。你在4点询问并在5点采取行动。与此同时,其他人抓住了门票。这是一种竞争条件 - 特别是竞争条件下的"检查然后行动"情景。
How do you detect them?
宗教代码审查,多线程单元测试。没有捷径。这个Eclipse插件很少出现,但还没有稳定。
How do you handle and prevent them?
最好的方法是创建无副作用和无状态函数,尽可能使用不可变的函数。但这并非总是可行的。因此,使用java.util.concurrent.atomic,并发数据结构,正确的同步和基于actor的并发性将有所帮助。
最佳的并发资源是JCIP。您还可以在此处获得有关上述说明的更多详细信息。
种族条件和数据竞赛之间存在重要的技术差异。大多数答案似乎都假设这些术语是等价的,但事实并非如此。
当2个指令访问相同的存储器位置时发生数据竞争,这些访问中的至少一个是写入,并且在这些访问中进行排序之前没有发生。现在,在排序之前构成一个事件的问题会受到很多争论,但一般来说,同一个锁变量上的ulock-lock对和同一个条件变量上的等待信号对会导致一个先发生的顺序。
竞争条件是语义错误。这是导致错误的程序行为的事件的时间或顺序中发生的缺陷。
许多竞争条件可能(实际上是)由数据竞争引起,但这不是必需的。事实上,数据竞赛和竞争条件既不是必要的,也不是彼此的充分条件。这篇博文还通过简单的银行交易示例很好地解释了这种差异。这是另一个解释差异的简单例子。
现在我们确定了术语,让我们尝试回答原始问题。
鉴于竞争条件是语义错误,没有检测它们的一般方法。这是因为在一般情况下无法使用能够区分正确与不正确程序行为的自动化oracle。种族检测是一个不可判定的问题。
另一方面,数据竞赛具有精确的定义,不一定与正确性相关,因此可以检测它们。有许多种类的数据竞争检测器(静态/动态数据竞争检测,基于锁定的数据竞争检测,基于发生的数据竞争检测,混合数据竞争检测)。最先进的动态数据竞争检测器是ThreadSanitizer,它在实践中运行良好。
处理数据竞争通常需要一些编程规则来诱导在共享数据访问之前发生 - 在开发期间或使用上述工具检测到它们之后。这可以通过锁,条件变量,信号量等来完成。但是,也可以采用不同的编程范例,如消息传递(而不是共享内存),以避免构造中的数据争用。
一种规范的定义是"当两个线程同时访问内存中的相同位置时,并且至少一个访问是写入。"在这种情况下,"reader"线程可能会获得旧值或新值,具体取决于哪个线程"赢得比赛"。这并不总是一个事实,一些真正有毛的低级算法是故意这样做的,但通常应该避免。 @Steve Gury给出了一个很好的例子,它可能是一个问题。
竞争条件是一种只在某些时间条件下发生的错误。
例:
想象一下,你有两个线程,A和B.
在线程A中:
1 2
| if( object.a != 0 )
object.avg = total / object.a |
在线程B中:
如果线程A在检查到object.a不为空之后被抢占,则B将执行a = 0,并且当线程A将获得处理器时,它将执行"除以零"。
只有当线程A在if语句之后被抢占时才会发生此错误,这种情况非常罕见,但它可能会发生。
竞争条件不仅与软件有关,而且与硬件有关。实际上这个术语最初是由硬件行业创造的。
根据维基百科:
The term originates with the idea of two signals racing each other to
influence the output first.
Race condition in a logic circuit:
理解这个术语的软件行业没有修改,这使得它有点难以理解。
您需要进行一些替换以将其映射到软件世界:
-
"两个信号"=>"两个线程"/"两个进程"
-
"影响输出"=>"影响某些共享状态"
因此,软件行业的竞争条件意味着"两个线程"/"两个进程"相互竞争"影响某些共享状态",共享状态的最终结果将取决于一些微妙的时序差异,这可能是由某些特定的线程/进程启动顺序,线程/进程调度等
竞争条件发生在多线程应用程序或多进程系统中。最基本的竞争条件是假设两个不在同一个线程或过程中的事物将按特定顺序发生,而不采取措施确保它们这样做。当两个线程通过设置和检查类都可以访问的成员变量来传递消息时,通常会发生这种情况。当一个线程调用sleep以给另一个线程时间来完成一个任务时,几乎总是存在竞争条件(除非该休眠处于循环中,具有一些检查机制)。
用于防止竞争条件的工具取决于语言和操作系统,但是一些常见的工具是互斥锁,关键部分和信号。当你想要确保你是唯一一个做某事的人时,互斥体是好的。当你想确保其他人已经完成某些事情时,信号很好。最小化共享资源还可以帮助防止意外行为
检测竞争条件可能很困难,但有几个迹象。严重依赖睡眠的代码容易出现竞争条件,因此请首先检查受影响代码中的睡眠呼叫。添加特别长的睡眠也可以用于调试以尝试强制特定的事件顺序。这可以用于重现行为,看看是否可以通过改变事物的时间使其消失,以及测试解决方案。调试后应该删除睡眠。
但是,如果存在仅在某些机器上间歇性地发生的问题,那么签名符号就是竞争条件。常见的错误是崩溃和死锁。通过日志记录,您应该能够找到受影响的区域并从那里开始工作。
竞争条件是并发编程的情况,其中两个并发线程或进程竞争资源,结果最终状态取决于谁首先获取资源。
微软实际上发布了一篇关于竞争条件和死锁问题的非常详细的文章。其中摘要最多的摘要是标题段落:
A race condition occurs when two threads access a shared variable at
the same time. The first thread reads the variable, and the second
thread reads the same value from the variable. Then the first thread
and second thread perform their operations on the value, and they race
to see which thread can write the value last to the shared variable.
The value of the thread that writes its value last is preserved,
because the thread is writing over the value that the previous thread
wrote.
What is a race condition?
当过程严重依赖于其他事件的顺序或时间时的情况。
例如,
处理器A和处理器B都需要相同的资源来执行它们。
How do you detect them?
有一些工具可以自动检测竞争条件:
-
基于锁定的竞赛检查器
-
发生在种族检测之前
-
混合种族检测
How do you handle them?
竞争条件可以由Mutex或Semaphores处理。它们充当锁定允许进程基于某些要求获取资源以防止竞争条件。
How do you prevent them from occurring?
有各种方法来预防竞争条件,例如关键部分规避。
在他们的关键区域内没有两个同时处理。 (互相排斥)
没有关于速度或CPU数量的假设。
没有进程在其关键区域之外运行,阻止其他进程。
没有进程必须永远等待进入其关键区域。 (A等待B资源,B等待C资源,C等待A资源)
以下是经典的银行账户余额示例,它将帮助新手轻松了解Java中的线程w.r.t.比赛条件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| 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));
} |
竞争条件是当设备或系统试图同时执行两个或多个操作时发生的不期望的情况,但是由于设备或系统的性质,操作必须以正确的顺序进行以便是做得对。
在计算机存储器或存储器中,如果几乎在同一时刻接收到读取和写入大量数据的命令,则机器会尝试覆盖部分或全部旧数据,而旧数据仍在读。结果可能是以下一种或多种:计算机崩溃,"非法操作",程序通知和关闭,读取旧数据的错误或写入新数据的错误。
如果使用"原子"类,则可以防止竞争条件。原因只是线程没有单独的操作get和set,例如如下:
1 2
| AtomicInteger ai = new AtomicInteger(2);
ai.getAndAdd(5); |
因此,您将在链接"ai"中有7个。
虽然你做了两个动作,但是这两个操作确认了同一个线程并且没有其他任何一个线程会干扰这个,这意味着没有竞争条件!
竞争条件是当两个或多个进程可以同时访问和更改共享数据时发生的不良情况。发生这种情况是因为存在对资源的冲突访问。临界区问题可能会导致竞争状况。为了解决过程中的关键条件,我们一次只取出一个执行临界区的过程。
你并不总是想放弃竞争条件。如果你有一个可以被多个线程读取和写入的标志,并且这个标志被一个线程设置为'done',那么当flag设置为'done'时其他线程停止处理,你不希望那个"比赛"条件"被淘汰。事实上,这个可以被称为良性竞争条件。
但是,使用检测竞争条件的工具,它将被视为有害的竞争条件。
有关种族状况的更多详细信息,请访问http://msdn.microsoft.com/en-us/magazine/cc546569.aspx。
考虑一旦计数增加就必须显示计数的操作。即,一旦CounterThread递增值,??DisplayThread就需要显示最近更新的值。
产量
1 2 3 4 5 6
| CounterThread -> i = 1
DisplayThread -> i = 1
CounterThread -> i = 2
CounterThread -> i = 3
CounterThread -> i = 4
DisplayThread -> i = 4 |
这里CounterThread经常获取锁定并在DisplayThread显示之前更新值。这里存在种族条件。竞争条件可以通过使用Synchronzation来解决
尝试这个基本示例以更好地了解竞争条件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136
| 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;
}
}
} |