为何多线程程序输出需Thread.Join或移除Thread.Sleep才稳定?
线程自增操作的同步问题分析
测试代码
using System; using System.Collections.Generic; using System.Threading; class Program { static volatile int a = 0; static void Main() { List<Thread> threads = new List<Thread>(); for (int i = 0; i < 1000; i++) { var t = new Thread(() => { a++; Thread.Sleep(1000); }); t.Start(); threads.Add(t); } // foreach(Thread t in threads) t.Join(); Thread.Sleep(60000); Console.WriteLine(a); Console.ReadKey(); } }
问题现象
- 保留
Thread.Sleep(1000)且注释t.Join()循环时,即使等待60秒,Console.WriteLine(a)的输出仍小于1000; - 取消
t.Join()循环注释或移除Thread.Sleep(1000)后,输出始终为1000。
疑问解答
1. 为何保留Thread.Sleep(1000)且注释t.Join()时输出不稳定?
核心原因有两点:
a++不具备原子性:a++本质是三个独立的原子操作:读取a的当前值、执行加1计算、写回新值。volatile关键字仅能保证变量的读写可见性,无法将这三个步骤绑定为一个不可打断的原子操作。- 高并发放大竞态冲突:线程执行完
a++后立刻进入1秒休眠,此时线程仍处于活跃状态,操作系统会同时调度最多1000个线程执行a++操作。多个线程极有可能同时读取到a的旧值,各自加1后写回,最终导致多次自增只生效一次(比如两个线程都读取到a=0,加1后都写回1,相当于只完成了1次自增)。
主线程等待60秒只能保证所有线程执行完毕,但无法解决多线程同时操作带来的数值覆盖问题,因此输出会小于1000。
2. 为何使用t.Join()或移除Thread.Sleep(1000)后输出能稳定为1000?
移除
Thread.Sleep(1000)的情况:
线程执行完a++后立刻终止,操作系统无需同时维持1000个活跃线程,而是会分批调度线程执行。a++操作的并发度大幅降低,多个线程同时读取a旧值的概率几乎为0,因此实际测试中a能准确累加到1000(注:理论上仍存在极小概率的竞态,但1000次操作下很难触发)。使用
t.Join()的情况:t.Join()让主线程等待所有子线程执行完毕后再输出a,但这不是解决竞态的核心原因。真正的关键是:主线程调用Join()时,线程调度的优先级和时机发生变化,再加上Thread.Sleep(1000)触发的内存屏障,间接降低了a++操作的并发冲突概率,使得竞态条件几乎不会发生,最终输出稳定为1000。
内容的提问来源于stack exchange,提问作者Minimus Heximus
相关产品推荐
相关产品推荐

