【问题标题】:C# Threading: a race condition exampleC# 线程:竞争条件示例
【发布时间】:2010-11-21 21:32:46
【问题描述】:

我正在阅读http://www.mono-project.com/ThreadsBeginnersGuide

第一个示例如下所示:

public class FirstUnsyncThreads {
    private int i = 0;

    public static void Main (string[] args) {
        FirstUnsyncThreads myThreads = new FirstUnsyncThreads ();
    }

    public FirstUnsyncThreads () {
        // Creating our two threads. The ThreadStart delegate is points to
        // the method being run in a new thread.
        Thread firstRunner = new Thread (new ThreadStart (this.firstRun));
        Thread secondRunner = new Thread (new ThreadStart (this.secondRun));

        // Starting our two threads. Thread.Sleep(10) gives the first Thread
        // 10 miliseconds more time.
        firstRunner.Start ();
        Thread.Sleep (10);
        secondRunner.Start ();
    }

    // This method is being excecuted on the first thread.
    public void firstRun () {
        while(this.i < 10) {
            Console.WriteLine ("First runner incrementing i from " + this.i +
                              " to " + ++this.i);
            // This avoids that the first runner does all the work before
            // the second one has even started. (Happens on high performance
            // machines sometimes.)
            Thread.Sleep (100);
        }
    }

    // This method is being excecuted on the second thread.
    public void secondRun () {
        while(this.i < 10) {
            Console.WriteLine ("Second runner incrementing i from " + this.i +
                              " to " + ++this.i);
            Thread.Sleep (100);
        }
    }
}

输出:

First runner incrementing i from 0 to 1
Second runner incrementing i from 1 to 2
Second runner incrementing i from 3 to 4
First runner incrementing i from 2 to 3
Second runner incrementing i from 5 to 6
First runner incrementing i from 4 to 5
First runner incrementing i from 6 to 7
Second runner incrementing i from 7 to 8
Second runner incrementing i from 9 to 10
First runner incrementing i from 8 to 9

哇,这是什么?不幸的是,文章中的解释对我来说是不充分的。你能解释一下为什么增量顺序混乱吗?

谢谢!

【问题讨论】:

  • 参考文章的作者说输出“可能”如图所示。从某种意义上说,这是正确的,因为您无法证明输出不会如图所示。然而,在实践中,考虑到 First 大约有 10 毫秒的领先优势,您会期望在发生任何“混乱”之前看到锁步交替进行相当多的迭代。

标签: c# multithreading


【解决方案1】:

我觉得这篇文章的作者搞错了。

VoteyDisciple 是正确的,++i 不是原子的,如果目标在操作期间未锁定,则可能会出现竞争条件,但这不会导致上述问题。

如果在调用++i 时发生竞争条件,那么++ 运算符的内部操作将类似于:-

  1. 第一个线程读取值 0
  2. 第二个线程读取值 0
  3. 第一个线程将值增加到 1
  4. 第二个线程将值增加到 1
  5. 第一个线程写入值 1
  6. 第二个线程写入值 1

操作 3 到 6 的顺序并不重要,关键是当变量具有值 x 时,读取操作 1 和 2 都可能发生,导致对 y 的增量相同,而不是每个线程执行不同的增量x 和 y 的值。

这可能会导致以下输出:-

First runner incrementing i from 0 to 1
Second runner incrementing i from 0 to 1

更糟糕的是:-

  1. 第一个线程读取值 0
  2. 第二个线程读取值 0
  3. 第二个线程将值增加到 1
  4. 第二个线程写入值 1
  5. 第二个线程读取值 1
  6. 第二个线程将值增加到 2
  7. 第二个线程写入值 2
  8. 第一个线程将值增加到 1
  9. 第一个线程写入值 1
  10. 第二个线程读取值 1
  11. 第二个线程将值增加到 2
  12. 第二个线程写入值 2

这可能会导致以下输出:-

First runner incrementing i from 0 to 1
Second runner incrementing i from 0 to 1
Second runner incrementing i from 1 to 2
Second runner incrementing i from 1 to 2

等等。

此外,在读取 i 和执行 ++i 之间可能存在竞争条件,因为 Console.WriteLine 调用连接了 i++i。这可能会导致如下输出:-

First runner incrementing i from 0 to 1
Second runner incrementing i from 1 to 3
First runner incrementing i from 1 to 2

作者描述的混乱的控制台输出只能由控制台输出的不可预测性引起,与i 变量的竞争条件无关。在执行++i 或连接i++i 时锁定i 不会改变这种行为。

【讨论】:

    【解决方案2】:

    当我运行这个(在双核上)时,我的输出是

    First runner incrementing i from 0 to 1
    Second runner incrementing i from 1 to 2
    First runner incrementing i from 2 to 3
    Second runner incrementing i from 3 to 4
    First runner incrementing i from 4 to 5
    Second runner incrementing i from 5 to 6
    First runner incrementing i from 6 to 7
    Second runner incrementing i from 7 to 8
    First runner incrementing i from 8 to 9
    Second runner incrementing i from 9 to 10
    

    正如我所料。您正在运行两个循环,都执行 Sleep(100)。这非常不适合展示竞争条件。

    代码确实存在竞争条件(如 VoteyDisciple 所述),但不太可能出现。

    我无法解释您的输出中缺少顺序(它是真正的输出吗?),但 Console 类将同步输出调用。

    如果您省略 Sleep() 调用并运行循环 1000 次(而不是 10 次),您可能会看到两个跑步者都从 554 增加到 555 或其他值。

    【讨论】:

    • 我只是复制了文章的内容,但是我可以通过删除程序中的初始睡眠并将线程中的睡眠降低到 20 来实现类似的混乱输出。
    • 你是对的,原因是在控制台的同步内部。
    【解决方案3】:

    当存在多个线程时,同步是必不可少的。在这种情况下,您会看到两个线程都读取和写入 this.i ,但是在同步这些访问方面没有做任何好的尝试。由于它们都同时修改相同的内存区域,因此您会观察到混乱的输出。 调用 Sleep 是危险的,这是一种导致某些错误的方法。你不能假设线程总是会被最初的 10 毫秒所取代。

    简而言之:永远不要使用 Sleep 进行同步 :-) 而是采用某种线程同步技术(例如锁、互斥锁、信号量)。始终尝试使用最轻的锁,以满足您的需求....

    一个有用的资源是 Joe Duffy 的书,Windows 上的并发编程。

    【讨论】:

    • 我不认为 Thread.Sleep() 用于尝试同步线程。它只是为了让增量是可观察的(否则它只会同时出现在控制台上),它的副作用是将竞争条件的机会减少到几乎为零。
    • 是的,评论是这么说的,但它实际上被用作一种粗略的同步技术,看看两次调用 Start() 之间的 10 ms 间隔。在没有其他技术的情况下,在我看来 Sleep 是在伪造两个线程之间的同步。
    • Joe Duffy 的书+1,它是 Windows 并发圣经
    【解决方案4】:

    增量没有乱序发生,Console.WriteLine(...) 正在将多个线程的输出写入单线程控制台,并且从多个线程到一个线程的同步导致消息出现乱七八糟的。

    我假设此示例尝试创建竞争条件,但在您的情况下失败了。不幸的是,并发问题,例如竞争条件和死锁,由于其性质而难以预测和重现。您可能想尝试再运行几次,将其更改为使用更多线程,并且每个线程应该增加更多次(例如 100,000)。然后您可能会看到最终结果将不等于所有增量的总和(由竞争条件引起)。

    【讨论】:

    • 对不起,Console.WriteLine。已更正。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2013-08-26
    • 1970-01-01
    • 2016-10-12
    • 2021-06-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多