Interlocked.Increment统计Parallel.ForEach线程数不符及HashSet替代疑问
问题解答
一、为什么threadsCount始终大于threads.Count?
threadsCount通过Interlocked.Increment每次进入委托执行时就加1,它统计的是Parallel.ForEach执行的任务总数量(也就是productConfig.Documenti.Keys中元素的总数)。HashSet<int> threads存储的是实际参与执行的线程ID,但Parallel.ForEach依赖.NET线程池,线程会被复用——同一个线程会连续执行多个任务。由于HashSet不允许重复元素,每个线程ID只会被成功添加一次,所以threads.Count是实际用到的不同线程的数量。- 当任务总数(即
threadsCount)大于线程池分配的线程数时,threadsCount自然会远大于threads.Count。另外HashSet本身非线程安全,多个线程同时调用Add可能导致部分添加操作失败(比如两个线程同时添加同一ID时,其中一个会返回false但不抛异常),这也会让threads.Count比实际参与的线程数更少,进一步拉大两者差距。
二、为什么应该用Interlocked而非HashSet这类标准集合?
1. 线程安全差异
Interlocked类的操作(如Increment、Decrement)是硬件级别的原子操作,能保证多线程环境下对整数变量的操作不会出现竞态条件(比如两个线程同时加1却只生效一次的情况)。HashSet、List这类标准集合都是非线程安全的,多个线程同时调用Add、Remove甚至遍历,都会破坏集合内部结构(比如哈希表的桶结构、链表节点),可能导致数据丢失、重复元素无法过滤,甚至抛出InvalidOperationException异常。
2. 用途与效率差异
Interlocked专门用于多线程下的简单数值同步,开销极小,适合任务计数、活跃线程统计这类场景。- 如果非要用集合统计线程ID,必须手动加锁(比如
lock(threads) { threads.Add(...); }),但加锁的开销远大于原子操作,代码也更繁琐。而且集合的核心用途是存储元素,用它做计数属于工具错配。
修正示例:统计并行执行的峰值线程数
如果你的需求是统计Parallel.ForEach运行过程中同时活跃的最大线程数,可以参考以下代码:
int activeThreads = 0; int maxActiveThreads = 0; HashSet<int> usedThreads = new HashSet<int>(); object lockObj = new object(); Parallel.ForEach(productConfig.Documenti.Keys, new ParallelOptions { MaxDegreeOfParallelism = MaxNumOfThreads }, key => { // 进入任务时增加活跃线程数 int currentActive = Interlocked.Increment(ref activeThreads); // 更新峰值线程数 Interlocked.CompareExchange(ref maxActiveThreads, currentActive, maxActiveThreads); // 加锁记录用到的线程ID,保证线程安全 lock(lockObj) { usedThreads.Add(System.Threading.Thread.CurrentThread.ManagedThreadId); } try { // 此处编写你的业务逻辑 } finally { // 任务结束时减少活跃线程数 Interlocked.Decrement(ref activeThreads); } }); Console.WriteLine($"总任务数:{productConfig.Documenti.Keys.Count}"); Console.WriteLine($"实际用到的线程总数:{usedThreads.Count}"); Console.WriteLine($"峰值活跃线程数:{maxActiveThreads}");
内容的提问来源于stack exchange,提问作者FDB
相关产品推荐
相关产品推荐

