多线程统计数组奇偶数字时计数总和异常的问题排查
多线程统计数组奇偶数字时计数总和异常的问题排查
你猜的没错,这个问题的根源就是共享变量low和up带来的竞态条件!咱们一步步拆解问题,再给出靠谱的解决办法。
问题根源分析
你的代码里,两个线程分别从数组两端向中间遍历,用low和up标记当前处理位置,但这两个变量是全局共享的,而且循环条件判断和循环体执行的整个流程不是原子操作,就会出现以下这种导致重复统计的场景:
假设当前low = k,up = k+1:
- 线程t1先判断
low < up成立,进入循环体但还没执行low++; - 这时候线程t2也判断
up > low成立,进入循环体并先执行了up--,把up改成了k; - 现在两个线程都会处理索引为
k的元素——t1处理numbers[low](也就是k),t2处理numbers[up](也是k); - 同一个元素被统计了两次,最终总和就会变成
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
相关产品推荐
相关产品推荐

