什么是比赛条件?


980

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

我对社区的问题是:

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


3
Linux安全编程HOWTO中有一章非常好,描述了它们是什么以及如何避免它们。
Craig H

4
我想提到的是-在没有指定语言的情况下-无法正确回答该问题的大部分内容,因为在不同的语言中,其定义,后果和防止其使用的工具可能会有所不同。
MikeMB 2015年

@MikeMB。同意,除了在分析字节码执行时(如Race Catcher所做的一样)(请参见该线程stackoverflow.com/a/29361427/1363844),我们可以解决所有大约62种可编译为字节码的语言(请参见en.wikipedia.org)。 /维基/ List_of_JVM_languages

Answers:


1236

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

当一个线程执行“先检查后执行”操作(例如,如果值是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

121
另一个线程遇到锁时会做什么?等待吗?错误?
布赖恩·奥尔蒂斯

173
是的,另一个线程必须等到锁释放后才能继续。这使得在保持线程完成时由保持线程释放锁非常重要。如果它从不释放它,则另一个线程将无限期等待。
Lehane

2
@Ian在多线程系统中,总有一些时候需要共享资源。如果说一种方法不好而不给出替代方案,那是徒劳的。我一直在寻找改善的方法,如果有其他选择,我会很乐意进行研究并权衡利弊。
Despertar

2
@Despertar ...也不一定是在多线程系统中总是需要共享资源的情况。例如,您可能有一个数组,其中每个元素都需要处理。您可以对数组进行分区,并且每个分区都有一个线程,线程可以完全独立地完成其工作。
伊恩·沃伯顿

12
对于一场竞赛,只要一个线程尝试更改共享数据而其余线程可以读取或更改它就足够了。
SomeWittyUsername 2012年

213

当将访问共享资源的多线程(或并行)代码以某种方式引起意外结果时,就会出现“竞争条件”。

举个例子:

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中存储相同的值(x + 1)!

例:

线程1:读取x,值为7
线程1:x加1,现在的值是8
线程2:读取x,值为7
线程1:在x中存储8
线程2:x加1,现在的值为8
线程2:在x中存储8

通过在访问共享资源的代码之前采用某种锁定机制,可以避免出现竞争情况:

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

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

有关锁定的更多信息,请搜索:互斥,信号量,关键部分,共享资源。


请参阅jakob.engbloms.se/archives/65中的程序示例,以测试这种情况如何恶化……这实际上取决于您所运行的计算机的内存模型。
jakobengblom2

1
如果必须停止在1000万,它将如何达到5000万?

9
@nocomprende:由5个线程一次执行相同的代码,如代码段正下方所述……
Jon Skeet 2015年

4
@JonSkeet你是对的,我把i和x弄混了。谢谢。

在实现Singleton模式中进行双重检查锁定就是防止竞争情况的示例。
巴拉特·多德雅

150

什么是比赛条件?

您打算在下午5点去看电影。您在下午4点询问门票的供应情况。该代表说,他们有空。放映前5分钟,您可以放松身心并到达售票窗口。我敢肯定,您可以猜测会发生什么:这是一间完整的房子。这里的问题在于检查和操作之间的持续时间。您在4咨询并在5采取行动。与此同时,其他人则抢了票。那是比赛条件-特别是比赛条件的“先检查后行动”场景。

您如何检测到它们?

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

您如何处理和预防它们?

最好的办法是创建无副作用的无状态函数,并尽可能多地使用不可变对象。但这并不总是可能的。因此,使用java.util.concurrent.atomic,并发数据结构,正确的同步以及基于actor的并发性将有所帮助。

最佳的并发资源是JCIP。您还可以在此处获得有关上述说明的更多详细信息


代码审查和单元测试是对耳朵之间的流进行建模的次要条件,并且较少使用共享内存。
Acumenus

2
我赞赏种族条件的真实示例
Tom O.

11
喜欢答案竖起大拇指。解决方案是:使用互斥锁(互斥,C ++)将票证锁定在4-5之间。在现实世界中,这称为票务预订:)
Volt

1
如果您删除仅Java的位(问题不是关于Java,而是一般的竞争条件),这将是一个不错的答案
Corey Goldberg,

不,这不是比赛条件。从“业务”的角度来看,您只是等待了太长时间。显然,缺货不是解决方案。尝试使用黄牛,否则只需购买机票作为保险
csherriff

65

竞争条件和数据竞争之间存在重要的技术差异。大多数答案似乎都假设这些术语是等效的,但事实并非如此。

当2条指令访问相同的存储器位置时发生数据争用,这些访问中的至少一个是写操作,并且在这些访问之间进行排序之前没有发生任何情况。现在,关于在顺序之前发生什么事情的争论很多,但是通常在同一锁定变量上的ulock-lock对和在同一条件变量上的wait-signal对会引发先发生事件。

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

许多竞争条件可能是(实际上是)数据竞争引起的,但这不是必需的。实际上,数据争用和竞争条件既不是彼此的必要条件也不是充分条件。这篇博客文章还通过一个简单的银行交易示例很好地解释了差异。这是另一个简单的示例,解释了它们之间的区别。

现在我们已经确定了术语,让我们尝试回答原始问题。

由于种族条件是语义错误,因此没有检测它们的通用方法。这是因为在一般情况下,无法使用自动的oracle来区分正确的程序行为与错误的程序行为。种族检测是一个无法确定的问题。

另一方面,数据争用具有不一定与正确性相关的精确定义,因此人们可以检测到它们。数据竞争检测器有很多种类(静态/动态数据竞争检测,基于锁集的数据竞争检测,基于事前发生的数据竞争检测,混合数据竞争检测)。最先进的动态数据竞争检测器是ThreadSanitizer,在实践中效果很好。

通常,处理数据争用需要一定的编程纪律,以在共享数据的访问之间(在开发期间或使用上述工具检测到它们之间)之间的边缘之前引发事件。这可以通过锁,条件变量,信号量等来完成。但是,也可以采用不同的编程范例,例如消息传递(而不是共享内存)来避免构造过程中的数据争用。


差异对于了解种族状况至关重要。谢谢!
ProgramCpp

37

规范的定义是“ 当两个线程同时访问内存中的相同位置,并且其中至少一个访问是写操作时”。在这种情况下,“阅读器”线程可能会获得旧值或新值,具体取决于哪个线程“赢得了比赛”。这并不总是一个错误-实际上,某些真正毛茸茸的低级算法是故意这样做的-但通常应避免这样做。@Steve Gury给出了一个很好的例子,说明何时可能出现问题。


3
您能否举一个例子,说明比赛条件如何有用?谷歌搜索没有帮助。
Alex V.

3
@Alex V.在这一点上,我不知道我在说什么。我认为这可能是对无锁编程的引用,但是说它本身取决于比赛条件并不太准确。
克里斯·康威

33

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

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

在线程A中:

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

在线程B中:

object.a = 0

如果在检查object.a不为null之后立即抢占线程A,则B会这样做a = 0,并且当线程A获得处理器时,它将执行“被零除”。

仅当在if语句之后抢占线程A时,才会发生此错误,这种情况非常罕见,但是有可能发生。


21

竞争状况不仅与软件有关,而且与硬件有关。实际上,该术语最初是由硬件行业创造的。

根据维基百科

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

逻辑电路中的竞争条件:

在此处输入图片说明

软件行业不加修改地使用了这个术语,这使得它有点难以理解。

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

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

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


20

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


只是一个很好的解释
gokareless

最终状态是什么?
Roman Alexandrovich

1
@RomanAlexandrovich程序的最终状态。指事物的状态,例如变量的值等。请参见Lehane的出色答案。在他的示例中,“状态”是指“ x”和“ y”的最终值。
AMTerp '19

19

竞争条件发生在多线程应用程序或多进程系统中。竞赛条件从最基本的意义上讲是一种假设,即不在同一线程或进程中的两件事将以特定顺序发生,而无需采取措施来确保它们做到了。当两个线程通过设置和检查两个类都可以访问的成员变量来传递消息时,通常会发生这种情况。当一个线程调用sleep来给另一个线程完成任务的时间时,几乎总是存在竞争状态(除非sleep处于循环中,并且具有某种检查机制)。

防止争用情况的工具取决于语言和操作系统,但一些常见的工具是互斥体,关键部分和信号。当您想确保自己是唯一做某事的人时,互斥体是很好的选择。当您要确保别人完成某件事时,信号是好的。最小化共享资源也可以帮助防止意外行为

检测比赛条件可能很困难,但是有一些迹象。严重依赖睡眠的代码容易出现竞争状况,因此请首先在受影响的代码中检查对睡眠的调用。添加特别长的睡眠也可以用于调试,以强制执行特定顺序的事件。这对于重现行为,查看是否可以通过更改事物的时间使其消失,以及测试已部署的解决方案很有用。调试后应删除睡眠。

但是,如果某个问题仅在某些机器上间歇性地出现,则表明它具有竞争状态。常见的错误是崩溃和死锁。使用日志记录,您应该能够找到受影响的区域并从那里进行工作。


10

微软实际上已经发布了有关种族条件和僵局问题的非常详细的文章。其中最概括的摘要是标题段落:

当两个线程同时访问一个共享变量时,就会发生竞争状态。第一个线程读取变量,第二个线程从变量读取相同的值。然后,第一个线程和第二个线程对值执行操作,然后争先看哪个线程可以最后将值写入共享变量。保留最后写入其值的线程的值,因为该线程将覆盖前一个线程写入的值。


5

什么是比赛条件?

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

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

您如何检测到它们?

有一些工具可以自动检测比赛状况:

您如何处理它们?

种族条件可以通过MutexSemaphores处理。它们充当锁,使进程可以根据某些要求来获取资源,以防止出现竞争状况。

您如何防止它们发生?

有多种防止竞争状况的方法,例如“避免临界区”

  1. 在其关键区域内没有两个进程同时进行。(互斥)
  2. 没有关于速度或CPU数量的假设。
  3. 没有任何进程在其关键区域之外运行会阻塞其他进程。
  4. 无需过程就永远等待其进入关键区域。(A等待B资源,B等待C资源,C等待A资源)

2

当设备或系统尝试同时执行两个或多个操作时,竞争状态是一种不希望出现的情况,但是由于设备或系统的性质,必须按照正确的顺序进行操作才能被执行。正确完成。

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


2

这是经典的银行帐户余额示例,它将帮助新手轻松理解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));

}

1

如果使用“原子”类,则可以防止出现竞争情况。原因是线程不分开操作get和set,示例如下:

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

结果,链接“ ai”中将有7个。尽管您执行了两个操作,但是两个操作都确认了同一线程,并且没有其他线程会干扰此操作,这意味着没有竞争条件!


0

请尝试以下基本示例,以更好地了解比赛条件:

    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;
        }
    }
}

0

您并不总是希望放弃竞争条件。如果您有一个可由多个线程读取和写入的标志,并且一个标志将该标志设置为“完成”,以便当标志设置为“完成”时其他线程停止处理,则您不希望该“竞赛”条件”。实际上,这一情况可以称为良性比赛条件。

但是,使用检测比赛状况的工具会发现它是有害的比赛状况。

有关比赛条件的更多详细信息,请参见http://msdn.microsoft.com/zh-cn/magazine/cc546569.aspx


您的答案基于什么语言?
MikeMB 2015年

坦白说,在我看来,如果您本身具有竞争条件,那么您并不是在以严格控制的方式来构建代码。从理论上讲,这可能不是问题,但可以证明设计和开发软件的方式存在较大问题。期望迟早要面对痛苦的比赛条件错误。
工程师

0

考虑一个操作,该操作必须在计数增加时立即显示计数。即,一旦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显示它之前更新该值。这里存在比赛条件。竞赛条件可以通过使用同步来解决


0

当两个或两个以上的进程可以同时访问和更改共享数据时,发生竞争情况是一种不受欢迎的情况。发生这种情况是因为对资源的访问冲突。关键部分问题可能会导致比赛状况。为了解决流程中的关键条件,我们一次只执行一个执行关键部分的流程。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.