C#线程:竞态条件示例
C#线程:竞态条件示例
我正在阅读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 () { // 创建两个线程。ThreadStart委托指向在新线程中运行的方法。 Thread firstRunner = new Thread (new ThreadStart (this.firstRun)); Thread secondRunner = new Thread (new ThreadStart (this.secondRun)); // 启动两个线程。Thread.Sleep(10)使第一个线程多得到10毫秒的时间。 firstRunner.Start (); Thread.Sleep (10); secondRunner.Start (); } // 此方法在第一个线程上执行。 public void firstRun () { while(this.i < 10) { Console.WriteLine ("First runner incrementing i from " + this.i + " to " + ++this.i); // 这样可以避免第一个线程在第二个线程开始之前完成所有工作。(在高性能机器上有时会发生。) Thread.Sleep (100); } } // 此方法在第二个线程上执行。 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
哇,这是什么?不幸的是,文章中的解释对我来说不充分。你能解释一下为什么递增的顺序会混乱吗?
谢谢!
C#多线程编程中,当存在多个线程时,同步是必不可少的。在这个例子中,可以看到两个线程都读写了this.i,但没有进行良好的同步。由于它们同时修改了同一块内存区域,所以输出结果混乱。
在这段代码中,调用Sleep是危险的,这种方法会导致确定的错误。你不能假设线程总是会在初始10毫秒内被替换。
简而言之,永远不要使用Sleep进行同步 🙂 而是采用某种线程同步技术(例如锁、互斥体、信号量)。总是试图使用最轻量级的锁来满足你的需求...
Joe Duffy的书《Concurrent Programming on Windows》是一个很好的资源。
我认为Thread.Sleep()并不是用来尝试同步线程的。它只是用来使增量可观察(否则所有的增量都会一次性显示在控制台上),这有一个副作用,几乎可以降低竞争条件发生的机会。
是的,评论中说过了,但实际上它被用作一种粗略的同步技术,看看两个Start()调用之间的10毫秒间隔。在没有其他技术的情况下,我认为Sleep伪装了两个线程之间的同步。
+1 Joe Duffy的书,它是Windows并发编程的圣经。
C#多线程:竞争条件示例
当我在一个双核处理器上运行这段代码时,输出结果如下:
第一个运行者将 i 从 0 增加到 1 第二个运行者将 i 从 1 增加到 2 第一个运行者将 i 从 2 增加到 3 第二个运行者将 i 从 3 增加到 4 第一个运行者将 i 从 4 增加到 5 第二个运行者将 i 从 5 增加到 6 第一个运行者将 i 从 6 增加到 7 第二个运行者将 i 从 7 增加到 8 第一个运行者将 i 从 8 增加到 9 第二个运行者将 i 从 9 增加到 10
正如我所预期的那样。你运行了两个循环,都执行了 Sleep(100)。这对于演示竞争条件来说非常不合适。
这段代码确实存在竞争条件(正如 VoteyDisciple 所描述的),但很不可能暴露出来。
我无法解释你输出结果的无序性(这是真实的输出吗?),但 Console 类会同步输出调用。
如果你省略 Sleep() 调用,并且将循环运行 1000 次(而不是 10 次),你可能会看到两个运行者都将 i 从 554 增加到 555 或其他值。
我只是复制了文章中的代码,然而通过删除程序中的初始 sleep 并将线程的 sleep 降低到 20,我也能够获得类似混乱的输出。
你是对的,原因在于 Console 的同步机制。
C#多线程:竞争条件示例
在上述内容中,出现了一个竞争条件问题。文章的作者混淆了问题的本质。VoteyDisciple正确地指出++i
不是原子操作,如果在操作期间没有对目标进行锁定,就会发生竞争条件,但这并不会导致上述描述的问题。
如果调用++i
时发生竞争条件,那么++
运算符的内部操作将如下所示:
- 第一个线程读取值为0
- 第二个线程读取值为0
- 第一个线程将值增加到1
- 第二个线程将值增加到1
- 第一个线程写入值为1
- 第二个线程写入值为1
操作3到6的顺序并不重要,重要的是读取操作1和2都可以在变量具有值x时发生,导致对y的相同递增,而不是每个线程对x和y的不同值进行递增。
这可能导致以下输出:
First runner incrementing i from 0 to 1 Second runner incrementing i from 0 to 1
更糟糕的是以下情况:
- 第一个线程读取值为0
- 第二个线程读取值为0
- 第二个线程将值增加到1
- 第二个线程写入值为1
- 第二个线程读取值为1
- 第二个线程将值增加到2
- 第二个线程写入值为2
- 第一个线程将值增加到1
- 第一个线程写入值为1
- 第二个线程读取值为1
- 第二个线程将值增加到2
- 第二个线程写入值为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
进行锁定也不会改变这种行为。