什么是比赛条件?

时间:2008-08-29 15:55:10

标签: multithreading concurrency terminology race-condition

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

我对社区的问题是:

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

18 个答案:

答案 0 :(得分:1073)

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

当一个线程执行“check-then-act”时经常会出现问题(例如,“检查”是否值为X,然后“执行”以执行取决于值为X的操作)并且另一个线程执行某些操作“检查”和“行为”之间的价值。 E.g:

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

答案 1 :(得分:194)

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

举个例子:

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

如果您有5个线程同时执行此代码,则x WOULD NOT的值最终为50,000,000。事实上每次运行都会有所不同。

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

Retrieve the value of x
Add 1 to this value
Store this value to x

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

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

示例:

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

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

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

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

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

答案 2 :(得分:131)

  

什么是比赛条件?

你计划在下午5点去看电影。您可以在下午4点查询门票的可用性。该代表说他们可以使用。您可以在演出前5分钟放松并到达售票窗口。我相信你可以猜到会发生什么:这是一个完整的房子。这里的问题是检查和行动之间的持续时间。你在4点询问并在5点采取行动。与此同时,其他人抓住了门票。这是一种竞争条件 - 特别是竞争条件的“检查然后行动”情景。

  

你如何发现它们?

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

  

你如何处理和预防它们?

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

最佳的并发资源是JCIP。您还可以获得更多details on above explanation here

答案 3 :(得分:60)

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

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

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

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

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

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

另一方面,数据竞赛有一个精确的定义,不一定与正确性有关,因此可以检测到它们。有许多种类的数据竞争检测器(静态/动态数据竞争检测,基于锁定的数据竞争检测,基于发生的数据竞争检测,混合数据竞争检测)。最先进的动态数据竞争检测器ThreadSanitizer在实践中非常有效。

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

答案 4 :(得分:33)

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

答案 5 :(得分:31)

竞争条件是一种只在某些时间条件下发生的错误。

实施例: 想象一下,你有两个线程,A和B.

在主题A中:

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

在主题B中:

object.a = 0

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

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

答案 6 :(得分:18)

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

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

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

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

答案 7 :(得分:16)

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

根据wikipedia

  

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

     

逻辑电路中的竞争条件:

     

enter image description here

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

您需要做一些替换才能将其映射到软件世界:

  • “两个信号”=&gt; “两个线程”/“两个进程”
  • “影响输出”=&gt; “影响一些共享国家”

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

答案 8 :(得分:9)

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

答案 9 :(得分:8)

微软实际上已经就竞争条件和僵局问题发表了非常详细的article。其中摘要最多的摘要是标题段落:

  

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

答案 10 :(得分:3)

  

什么是竞争条件?

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

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

  

你如何发现它们?

有自动检测竞争条件的工具:

  

你如何处理它们?

竞争条件可以通过 Mutex 信号量来处理。它们充当锁定允许进程根据某些要求获取资源以防止竞争条件。

  

你如何防止它们发生?

有多种方法可以预防种族状况,例如关键部分规避

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

答案 11 :(得分:2)

以下是经典的银行账户余额示例,它将帮助新手轻松了解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 stub
    BankAccount b = new BankAccount();
    b.accountBalance=2000;
    System.out.println(b.Withdraw(3000));

}

答案 12 :(得分:2)

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

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

答案 13 :(得分:0)

public class Synchronized_RACECONDITION {
    private static final int NUM_INCREMENTS = 10000;

    private static int count = 0;

    public static void main(String[] args) {
        testSyncIncrement();
        testNonSyncIncrement();
    }

    private static void testSyncIncrement() {
        count = 0;

        ExecutorService executor = Executors.newFixedThreadPool(2);

        IntStream.range(0, NUM_INCREMENTS)
                .forEach(i -> executor.submit(Synchronized_RACECONDITION::incrementSync));

        ConcurrentUtils.stop(executor);

        System.out.println("   Sync: " + count);
    }

    private static void testNonSyncIncrement() {
        count = 0;

        ExecutorService executor = Executors.newFixedThreadPool(2);

        IntStream.range(0, NUM_INCREMENTS)
                .forEach(i -> executor.submit(Synchronized_RACECONDITION::increment));

        ConcurrentUtils.stop(executor);

        System.out.println("NonSync: " + count);
    }

    private static synchronized void incrementSync() {
        count = count + 1;
    }

    private static void increment() {
        count = count + 1;
    }
static  class ConcurrentUtils {

    public static void stop(ExecutorService executor) {
        try {
            executor.shutdown();
            executor.awaitTermination(60, TimeUnit.SECONDS);
        }
        catch (InterruptedException e) {
            System.err.println("termination interrupted");
        }
        finally {
            if (!executor.isTerminated()) {
                System.err.println("killing non-finished tasks");
            }
            executor.shutdownNow();
        }
    }
}
}

答案 14 :(得分:0)

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

答案 15 :(得分: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 显示之前更新该值。这里存在种族条件。种族条件可以通过使用同步

来解决

答案 16 :(得分:0)

你并不总是想放弃竞争条件。如果您有一个可由多个线程读取和写入的标志,则此标志设置为“已完成”。通过一个线程使其他线程在标志设置为“完成”时停止处理,您不希望这种情况&#34;竞争条件&#34;要被淘汰事实上,这个可以被称为良性竞争条件。

然而,使用工具检测竞争状况,它将被视为有害的竞争条件。

有关种族状况的更多详情,http://msdn.microsoft.com/en-us/magazine/cc546569.aspx

答案 17 :(得分: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;
        }
    }
}