为何锁定的Parallel.Foreach不会立即死锁?及并行循环死锁疑问
为什么Parallel.Foreach会引发死锁,且执行次数超预期?
首先我得补全你大概率用到的测试代码(因为你贴的Test类没写完),假设你的代码框架是这样的:
class Test { private readonly object _lockObj = new object(); private int _internalCounter = 0; // 带锁的Getter public int Counter { get { lock (_lockObj) { // 模拟锁占用时间较长的操作 Thread.Sleep(50); return _internalCounter++; } } } public void RunTest() { // 传入包含2个元素的集合 var testItems = new List<int> { 1, 2 }; Parallel.ForEach(testItems, item => { Console.WriteLine($"线程 {Thread.CurrentThread.ManagedThreadId} 获取Counter值:{Counter}"); // 若此处还有其他锁操作,死锁概率会大幅提升 }); } }
接下来分两个核心问题拆解:
一、为什么Parallel.Foreach会死锁,普通for循环却不会?
你提到「嵌套锁不会引发问题」这个结论是对的——CLR的lock是可重入锁,同一个线程可以多次获取同一个锁,不会出现自己卡自己的情况。问题的核心在于Parallel.Foreach的多线程特性:
- 普通for循环是单线程串行执行:所有对
Counter的调用都来自同一个线程,哪怕getter里嵌套了多层锁,也不会有跨线程的锁竞争,自然不会死锁。 - Parallel.Foreach是多线程并行执行:它会从线程池调度多个线程处理集合元素。如果你的
Countergetter锁持有时间过长(比如示例里的Thread.Sleep(50)),再加上代码中存在交叉锁场景(比如线程A先锁了对象A再拿_lockObj,线程B先锁了_lockObj再拿对象A),就会形成经典的交叉锁死锁——两个线程各自持有对方需要的锁,互相等待,永远无法推进。
哪怕只有这一个锁,也可能出现「类死锁阻塞」:如果线程池线程被耗尽,持有锁的线程被挂起,其他等待锁的线程也无法继续,看起来和死锁表现一致。
二、为什么执行次数会超过两次?
集合明明只有2个元素,却出现超次数执行,大概率是这几个原因:
- 委托内多次调用了
Counter:如果Parallel.Foreach的委托逻辑里不止一次调用Counter(比如写了两次var val = Counter;),自然会触发更多次的getter执行。 - 锁对象错误导致的线程安全问题:如果你的锁对象是值类型(比如
int),或者是共享的静态对象,会导致多个独立的锁实例,多个线程能同时进入getter,_internalCounter被多次递增,看起来执行次数变多。 - Parallel分区器的特殊调度:对于极小集合(比如2个元素),默认分区器可能会创建多个分区,极端情况下会出现重复调度迭代任务(这种情况非常少见)。
修复建议
- 避免在Parallel.Foreach的并行委托里调用带锁方法,对于计数器这种场景,用更轻量的线程安全方案替代lock:
private int _internalCounter = 0; public int Counter => Interlocked.Increment(ref _internalCounter); - 如果必须用锁,严格保证所有线程获取锁的顺序一致,杜绝交叉锁场景。
- 把锁的范围缩到最小,只在必要的代码块上加锁,减少锁持有时间。
内容的提问来源于stack exchange,提问作者ChristianMurschall
相关产品推荐
相关产品推荐

