C#中Parallel.ForEach同名资源互斥执行的解决方案
解决方案:用ConcurrentDictionary结合SemaphoreSlim实现按资源串行、跨资源并行
核心思路是用线程安全的字典维护每个资源对应的信号量,同一资源共享一个初始计数为1的信号量,确保同一时间只有一个线程处理该资源;不同资源的信号量相互独立,实现跨资源并行。
具体代码实现
using System.Collections.Concurrent; using System.Threading; string[] arr = ["A", "B", "B", "B", "B", "B", "C"]; var options = new ParallelOptions() { MaxDegreeOfParallelism = 3 }; // 线程安全字典,存储每个资源对应的信号量 var resourceLocks = new ConcurrentDictionary<string, SemaphoreSlim>(); try { Parallel.ForEach(arr, options, res => { // 获取或创建当前资源对应的信号量(初始计数1,最多允许1个线程进入) var semaphore = resourceLocks.GetOrAdd(res, _ => new SemaphoreSlim(1, 1)); try { // 等待信号量,同一资源的线程会在此排队 semaphore.Wait(); // ------------------------- // 这里写处理res的业务代码 Console.WriteLine($"开始处理 {res} | 线程ID: {Thread.CurrentThread.ManagedThreadId}"); Thread.Sleep(1000); // 模拟耗时操作 Console.WriteLine($"完成处理 {res} | 线程ID: {Thread.CurrentThread.ManagedThreadId}"); // ------------------------- } finally { // 释放信号量,让下一个等待该资源的线程进入 semaphore.Release(); } }); } finally { // 清理所有信号量,避免资源泄漏 foreach (var kvp in resourceLocks) { kvp.Value.Dispose(); } resourceLocks.Clear(); }
为什么不用命名信号量?
命名信号量是系统级别的跨进程同步工具,适合多个进程间的资源互斥,但这里只需要进程内的同步,用它反而有以下问题:
- 需要手动维护唯一命名,容易出现命名冲突;
- 生命周期管理繁琐,需要手动释放,否则会残留系统资源;
- 性能比进程内的
SemaphoreSlim更重,没必要。
关键逻辑说明
ConcurrentDictionary<string, SemaphoreSlim>:线程安全的字典,确保多个线程同时访问同一资源时,只会创建一个信号量实例;SemaphoreSlim(1,1):初始计数为1,最大计数为1,等价于一个排他锁,同一时间只有一个线程能进入临界区;Wait()和Release():Wait()会阻塞当前线程直到信号量可用,处理完成后Release()释放信号量,让下一个等待的线程执行。
这样就能实现:A和B可以同时执行,多个B必须串行执行的需求,完全解决运行时动态创建锁的问题,不需要静态化任何资源。
内容的提问来源于stack exchange,提问作者Sameh
相关产品推荐
相关产品推荐

