You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

为何多线程程序输出需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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.16 00:52:34