ChatGPT解决这个技术问题 Extra ChatGPT

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

我对社区的问题是:

什么是比赛条件?

你如何检测它们?

你如何处理它们?

最后,如何防止它们发生?

我想提一下 - 在没有指定语言的情况下 - 这个问题的大部分部分都无法正确回答,因为在不同的语言中,定义、后果和防止它们的工具可能会有所不同。
@迈克MB。同意,除了在分析字节码执行时,就像 Race Catcher 所做的那样(参见这个线程 stackoverflow.com/a/29361427/1363844),我们可以处理所有那些大约 62 种编译为字节码的语言(参见 en.wikipedia.org/wiki/List_of_JVM_languages

A
Amit Joki

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

当一个线程执行“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

其他线程遇到锁怎么办?它等待吗?错误?
是的,另一个线程必须等到锁被释放才能继续。这使得锁在完成后由持有线程释放非常重要。如果它从不释放它,那么另一个线程将无限期地等待。
@Ian 在多线程系统中,总会有需要共享资源的时候。说一种方法不好而不给出替代方案是没有成效的。我一直在寻找改进的方法,如果有替代方法,我会很乐意对其进行研究并权衡利弊。
@Despertar ...此外,不一定总是需要在多线程系统中共享资源。例如,您可能有一个数组,其中每个元素都需要处理。您可以对数组进行分区并为每个分区设置一个线程,并且这些线程可以完全独立地完成它们的工作。
为了发生竞争,一个线程尝试更改共享数据而其余线程可以读取或更改它就足够了。
I
IKavanagh

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

举个例子:

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

如果您有 5 个线程同时执行此代码,则 x 的值最终不会是 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。

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


请参阅 jakob.engbloms.se/archives/65 以获取程序示例,以测试此类事情有多容易变坏……这实际上取决于您正在运行的机器的内存模型。
非要停在1000万,怎么能到5000万?
@nocomprende:由 5 个线程一次执行相同的代码,如代码段下方所述...
@JonSkeet你是对的,我混淆了 i 和 x。谢谢你。
实现单例模式的双重检查锁定就是防止竞争条件的一个例子。
e
ekostadinov

什么是竞态条件?

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

你如何检测它们?

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

您如何处理和预防它们?

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

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


代码审查和单元测试是次要的,而不是对你的耳朵之间的流程进行建模,并减少对共享内存的使用。
我很欣赏竞争条件的真实世界示例
喜欢答案竖起大拇指。解决方案是:您使用互斥锁(互斥,c++)将票证锁定在 4-5 之间。在现实世界中,它被称为订票:)
如果您放弃了仅限 java 的位,那将是一个不错的答案(问题不是关于 Java,而是一般的竞争条件)
不,这不是比赛条件。从“业务”的角度来看,您等待的时间太长了。显然,延期交货不是解决方案。尝试黄牛,否则只需购买门票作为保险
C
Community

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

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

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

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

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

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

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

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


差异对于了解比赛条件至关重要。谢谢!
“这是一个发生在时间或事件顺序中的缺陷,会导致错误的程序行为。”完美的定义!事实上,没有理由假设事件必须发生在应用程序的一个实例中。多个实例同样适用。
C
Chris Conway

一种规范的定义是“当两个线程同时访问内存中的同一位置,并且至少其中一个访问是写入时”。在这种情况下,“读者”线程可能会获得旧值或新值,这取决于哪个线程“赢得比赛”。这并不总是一个错误——事实上,一些非常复杂的低级算法是故意这样做的——但通常应该避免它。 @Steve Gury 给出了一个很好的例子,说明什么时候可能会出现问题。


您能否举例说明竞争条件如何有用?谷歌搜索没有帮助。
@Alex V. 在这一点上,我不知道我在说什么。我认为这可能是对无锁编程的参考,但说这取决于竞争条件本身并不准确。
B
Blorgbeard

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

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

在线程 A 中:

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

在线程 B 中:

object.a = 0

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

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


J
Jorge Córdoba

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


只是精彩的解释
最终状态是什么?
@RomanAlexandrovich 程序的最终状态。状态指的是变量值等事物。请参阅 Lehane 的出色回答。他示例中的“状态”指的是“x”和“y”的最终值。
C
Community

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

根据wikipedia

该术语起源于两个信号相互竞争以首先影响输出的想法。逻辑电路中的竞争条件:

软件行业不加修改就取了这个词,有点难理解。

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

“两个信号”=>“两个线程”/“两个进程”

“影响输出” => “影响某些共享状态”

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


t
tsellon

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

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

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

但是,如果存在仅在某些机器上间歇性发生的问题,则表明存在竞争条件的签名标志。常见的错误是崩溃和死锁。通过日志记录,您应该能够找到受影响的区域并从那里恢复工作。


K
Konstantin Dinev

Microsoft 实际上已经发布了关于竞争条件和死锁问题的非常详细的article。其中最总结的摘要将是标题段落:

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


T
Trieu Toan

什么是竞态条件?

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

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

你如何检测它们?

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

基于 Lockset 的竞赛检查器

发生在种族检测之前

混合种族检测

你如何处理它们?

竞争条件可以由 Mutex 或 Semaphores 处理。它们充当锁,允许进程根据某些要求获取资源以防止竞争条件。

你如何防止它们发生?

有多种方法可以防止竞争条件,例如避免关键部分。

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


d
dilbag koundal

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

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


V
Vishal Shukla

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

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

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


z
zacksiri

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

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

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

https://i.stack.imgur.com/rONSe.png

https://i.stack.imgur.com/a9TgU.jpg

你可以在这里查看。 https://youtu.be/RWRicNoWKOY


B
Blorgbeard

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

}

在存款方式上,如果金额为负值,人们可以正确存款
M
Morsu

尝试这个基本示例以更好地理解竞争条件:

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

k
kiriloff

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

但是,使用检测竞态条件的工具,会发现它是有害的竞态条件。

在此处了解有关竞争条件的更多详细信息,http://msdn.microsoft.com/en-us/magazine/cc546569.aspx


您的答案基于什么语言?
坦率地说,在我看来,如果你本身有竞争条件,你就没有以一种严格控制的方式来构建你的代码。虽然这在您的理论案例中可能不是问题,但它证明了您设计和开发软件的方式存在更大的问题。预计迟早会面临痛苦的比赛条件错误。
b
bharanitharan

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


r
rashedcs

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