C# Parallel.ForEach生成大素数时非素数误加入集合问题求解
问题根源
你的代码出现非素数被加入集合、输出混乱不是if判断被绕过,是存在多处线程安全竞态问题:
- 把存储随机数的
makeBytes字节数组、待检测的makePrime变量定义在了并行循环外层,所有工作线程共享这两个值。一个线程刚完成素数检测准备存值时,另一个线程可能已经修改了共享变量的内容,导致最终存入集合的根本不是通过素数检测的那个数。 - 初始的
condition变量只在循环启动前计算一次,后续计数变化不会更新这个值,加上原有迭代器逻辑无法及时响应停止信号,会导致循环多跑很多无效任务。 - 读取计数
trackNum时没有做内存同步,线程可能读到缓存的旧值,停止逻辑不及时。 ConcurrentBag是无序集合,TryPeek只能拿到当前线程本地队列的元素,根本不是当前刚加入的素数,自然无法保证输出顺序。
修正方案
核心修复原则是:CPU密集的素数检测环节全程用线程本地变量,完全避免共享;计数、输出这类必须共享的操作,用原子操作或极小粒度的锁保证安全。
- 所有临时变量(字节数组、待检测大整数)全部放到并行循环的迭代内部声明,每个线程用自己的副本,从根源上避免竞态
- 废弃原来传固定condition的迭代器,改用无限迭代源,完全靠
ParallelLoopState控制循环停止,计数操作用Interlocked做原子更新,读计数用Volatile.Read保证拿到最新值 - 不要用无序的
ConcurrentBag存值,素数检测通过后直接通过原子操作拿到当前素数的发现序号,用轻量锁保护控制台输出,既不损失多少性能,又能保证输出顺序和发现顺序一致 - 随机数生成器要么用.NET 6+自带的线程安全静态方法,老版本用
ThreadLocal为每个线程分配独立实例,不要共享实例
修正后的完整代码:
// .NET 5及以下版本取消注释下面这行,替换循环内的随机数生成逻辑 // private static readonly ThreadLocal<RandomNumberGenerator> _threadRng = new(() => RandomNumberGenerator.Create()); // 控制台输出专用锁,定义为类的静态只读字段即可 private static readonly object _consoleLock = new object(); public void PrintOutPrimes(int numPrimes) { int foundCount = 0; // 可根据需要调整最大并行度,默认全核心跑,留一个核给系统可以避免操作卡顿 var options = new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount - 1 }; Parallel.ForEach(InfiniteEnumerator(), options, (_, state) => { // 先判断是否已经凑够素数数量,及时停止 if (Volatile.Read(ref foundCount) >= numPrimes) { state.Stop(); return; } // 每个迭代单独创建字节数组,绝不和其他线程共享 byte[] randomBytes = new byte[512]; // .NET 6+ 直接用线程安全的静态方法生成非零随机字节 RandomNumberGenerator.GetNonZeroBytes(randomBytes); // .NET 5及以下版本用线程本地实例生成: // _threadRng.Value.GetNonZeroBytes(randomBytes); // 把最高字节最高位置0,保证生成的是正整数,比后续调用Abs更稳妥 randomBytes[^1] &= 0x7f; BigInteger candidate = new BigInteger(randomBytes); if (CheckForPrime(candidate)) { // 原子递增计数,拿到当前素数的发现序号 int currentSeq = Interlocked.Increment(ref foundCount); if (currentSeq > numPrimes) { state.Stop(); return; } // 锁粒度极小,只有找到素数要输出时才会进入,几乎不会有性能损失 lock (_consoleLock) { Console.WriteLine($"第{currentSeq}个素数:{candidate}"); } } }); } // 简单的无限迭代器,停止逻辑完全交给Parallel的状态控制 private static IEnumerable<bool> InfiniteEnumerator() { while (true) { yield return true; } }
补充说明
- 素数检测本身是CPU密集型操作,绝大多数随机数都会在检测环节被过滤掉,真正走到输出、计数逻辑的频次非常低,所以锁和原子操作的开销可以完全忽略,不会抵消并行带来的性能提升。
- 如果确实需要把找到的素数存起来后续用,可以在锁内部同时把素数按序号存到
List<BigInteger>里,比用并发集合更高效,还能保证顺序。
内容的提问来源于stack exchange,提问作者kodak67
相关产品推荐
相关产品推荐

