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

多线程统计数组奇偶数字时计数总和异常的问题排查

多线程统计数组奇偶数字时计数总和异常的问题排查

你猜的没错,这个问题的根源就是共享变量low和up带来的竞态条件!咱们一步步拆解问题,再给出靠谱的解决办法。

问题根源分析

你的代码里,两个线程分别从数组两端向中间遍历,用low和up标记当前处理位置,但这两个变量是全局共享的,而且循环条件判断和循环体执行的整个流程不是原子操作,就会出现以下这种导致重复统计的场景:
假设当前low = k,up = k+1:

  1. 线程t1先判断low < up成立,进入循环体但还没执行low++;
  2. 这时候线程t2也判断up > low成立,进入循环体并先执行了up--,把up改成了k;
  3. 现在两个线程都会处理索引为k的元素——t1处理numbers[low](也就是k),t2处理numbers[up](也是k);
  4. 同一个元素被统计了两次,最终总和就会变成N+1。

另外你问的「不使用lock或Interlocked增减公共变量是否安全」:在你的代码里,t1只改low,t2只改up,单变量的增减本身不会有写冲突,但问题出在循环条件同时依赖两个共享变量,而且条件判断和后续操作不是原子的,所以依然会出逻辑错误。

解决办法

方案1:拆分任务(最推荐,简单高效)

直接把数组分成两个独立区间,让两个线程各自处理自己的区间,完全不共享变量,最后合并结果。这种方式从根源上避免了线程竞争,效率最高,结果绝对准确:

public partial class Program
{
    const int N = 10000000;
    static int[] numbers = new int[N];

    // 每个线程独立处理指定区间,维护自己的计数
    static void CountRange(int start, int end, out int oddCount, out int evenCount)
    {
        oddCount = 0;
        evenCount = 0;
        for (int i = start; i <= end; i++)
        {
            if (numbers[i] % 2 == 0)
                evenCount++;
            else
                oddCount++;
        }
    }

    public static void Main(string[] args)
    {
        for (int i = 0; i < N; i++)
        {
            numbers[i] = i;
        }

        int mid = N / 2;
        int t1Odd = 0, t1Even = 0;
        int t2Odd = 0, t2Even = 0;

        Thread t1 = new Thread(() => CountRange(0, mid - 1, out t1Odd, out t1Even));
        t1.IsBackground = true;
        t1.Start();

        Thread t2 = new Thread(() => CountRange(mid, N - 1, out t2Odd, out t2Even));
        t2.IsBackground = true;
        t2.Start();

        t1.Join();
        t2.Join();

        int totalOdd = t1Odd + t2Odd;
        int totalEven = t1Even + t2Even;
        Console.WriteLine($"Odd numbers {totalOdd}, even numbers: {totalEven}");
    }
}

方案2:加锁保证原子性

给共享变量的访问和操作加锁,确保每次只有一个线程能执行循环的判断和处理逻辑。不过这种方式会让两个线程基本变成串行执行,失去多线程的意义,效率较低:

首先定义锁对象:

static object lockObj = new object();

然后修改两个统计方法:

static void CalculateLow()
{
    while (true)
    {
        lock (lockObj)
        {
            if (low >= up) break;
            if (numbers[low] % 2 == 0)
                Interlocked.Increment(ref numberOfEvenNumbers);
            else
                Interlocked.Increment(ref numberOfOddNumbers);
            low++;
        }
    }
}

static void CalculateUp()
{
    while (true)
    {
        lock (lockObj)
        {
            if (up <= low) break;
            if (numbers[up] % 2 == 0)
                Interlocked.Increment(ref numberOfEvenNumbers);
            else
                Interlocked.Increment(ref numberOfOddNumbers);
            up--;
        }
    }
}

方案3:原子操作管理指针(稍复杂)

如果你坚持要从两端向中间遍历,可以用Interlocked类的方法原子地更新low和up,确保每次获取和更新指针的操作都是原子的,不过这种方式依然可能存在极端场景的竞态,不如拆分任务稳妥:

static void CalculateLow()
{
    int currentLow;
    while ((currentLow = Interlocked.CompareExchange(ref low, low, low)) < up)
    {
        if (numbers[currentLow] % 2 == 0)
            Interlocked.Increment(ref numberOfEvenNumbers);
        else
            Interlocked.Increment(ref numberOfOddNumbers);
        Interlocked.Increment(ref low);
    }
}

static void CalculateUp()
{
    int currentUp;
    while ((currentUp = Interlocked.CompareExchange(ref up, up, up)) > low)
    {
        if (numbers[currentUp] % 2 == 0)
            Interlocked.Increment(ref numberOfEvenNumbers);
        else
            Interlocked.Increment(ref numberOfOddNumbers);
        Interlocked.Decrement(ref up);
    }
}

备注:内容来源于stack exchange,提问作者Bnc147

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.16 02:54:50