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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.29 02:45:35