.NET 6多线程异常:UpgradeableReadLock未持有却释放问题排查
.NET 6.0 多线程升级读锁偶发释放异常排查与解决建议
问题概述
在基于.NET 6.0的服务器异步方法LoadCell中,偶发出现SynchronizationLockException,提示**"The upgradeable lock is being released without being held"**。该问题无固定触发规律,服务器有时运行数天无异常,有时启动后即刻出现。异常发生前锁的进入/释放日志均能正常配对,异常触发时WriteLock未输出释放日志,随后执行ExitUpgradeableReadLock抛出异常。
问题代码片段
private async Task<Cell> LoadCell() { if (_loadingLock.TryEnterUpgradeableReadLock(new TimeSpan(0, 0, 0, 0, 20))) { _globalReadlockCountEnter++; Log.Info("_loadingLock Enter upgrade readlock, count: " + _globalReadlockCountEnter); try { if (_mapLock.TryEnterReadLock(new TimeSpan(0, 0, 0, 0, 20))) { try { //Try get value } finally { _mapLock.ExitReadLock(); } } else { throw new TimeoutException("Timeout trying to get read lock in level cell manager."); } bool acquired = false; try { if (!_loadingLock.TryEnterWriteLock(new TimeSpan(0, 0, 0, 20))) { throw new TimeoutException("Timeout trying to get write lock in level cell manager."); } else { acquired = true; _globalWritelockCountEnter++; Log.Info("_loadingLock Enter writelock, count: " + _globalWritelockCountEnter); //deal with certain value } } catch (Exception e) { throw new Exception("Unknown exception error."); } finally { if (acquired) { if (!_loadingLock.IsWriteLockHeld) { Log.Warn("write lock has not been held, check is everything alright?"); } _loadingLock.ExitWriteLock(); _globalWritelockCountExit++; Log.Info("_loadingLock exit writelock, count: " + _globalWritelockCountExit); } else { Log.Warn("acquire is false some how it failed to exit the write lock."); } } } finally { if (!_loadingLock.IsUpgradeableReadLockHeld) { Log.Warn("upgrade read lock has not been held, check is everything alright?"); } _loadingLock.ExitUpgradeableReadLock(); _globalReadlockCountExit++; Log.Info("_loadingLock Exit upgrade readlock, count: " + _globalReadlockCountExit); } } else { throw new TimeoutException("Timeout trying to get upgrade read lock in level cell manager."); } }
异常日志片段
2023-07-19 07:43:48,601 INFO _loadingLock Exit upgrade readlock, count: 1060 2023-07-19 07:43:48,601 INFO _loadingLock Enter upgrade readlock, count: 1061 2023-07-19 07:43:48,601 INFO _loadingLock Enter writelock, count: 358 2023-07-19 07:43:48,602 INFO _loadingLock exit writelock, count: 358 2023-07-19 07:43:48,602 INFO _loadingLock Exit upgrade readlock, count: 1061 2023-07-19 07:43:48,602 INFO _loadingLock Enter upgrade readlock, count: 1062 2023-07-19 07:43:48,602 INFO _loadingLock Enter writelock, count: 359 2023-07-19 07:43:48,603 INFO _loadingLock exit writelock, count: 359 2023-07-19 07:43:48,603 INFO _loadingLock Exit upgrade readlock, count: 1062 ... 2023-07-19 07:43:48,604 INFO _loadingLock Enter upgrade readlock, count: 1082 2023-07-19 07:43:48,604 INFO _loadingLock Enter writelock, count: 360 2023-07-19 07:43:48,623 WARN write lock has not been held, check is everything alright? 2023-07-19 07:43:48,623 WARN upgrade read lock has not been held, check is everything alright? Unhandled exception. System.Threading.SynchronizationLockException: The upgradeable lock is being released without being held.
问题根源分析
- 异步方法与同步锁的线程亲和冲突:
ReaderWriterLockSlim是线程亲和锁,锁的持有与释放必须在同一个线程上。但当前方法是async Task,await操作会导致线程切换,后续释放锁的线程可能不是获取锁的线程,直接破坏锁的状态,引发未持有锁却释放的异常——这是最核心的原因。 - 异常处理的掩盖问题:获取WriteLock的catch块直接抛出新异常,可能掩盖原始锁状态异常,导致无法追踪锁状态异常的真实触发点。
- 超时后的锁状态不确定性:
ReaderWriterLockSlim在超时后不会自动清理锁状态,异步场景下线程切换可能导致acquired标记与实际锁状态不一致。
解决方案建议
1. 替换为异步友好的锁机制
异步方法必须使用异步锁,推荐使用第三方库Nito.AsyncEx中的AsyncReaderWriterLock(.NET无内置异步读写锁),示例代码如下:
private readonly AsyncReaderWriterLock _loadingLock = new AsyncReaderWriterLock(); private readonly AsyncReaderWriterLock _mapLock = new AsyncReaderWriterLock(); private async Task<Cell> LoadCell() { using (await _loadingLock.UpgradeableReadLockAsync(TimeSpan.FromMilliseconds(20))) { _globalReadlockCountEnter++; Log.Info($"_loadingLock Enter upgrade readlock, count: {_globalReadlockCountEnter}, ThreadId: {Thread.CurrentThread.ManagedThreadId}"); try { using (await _mapLock.ReadLockAsync(TimeSpan.FromMilliseconds(20))) { // Try get value } using (await _loadingLock.WriteLockAsync(TimeSpan.FromSeconds(20))) { _globalWritelockCountEnter++; Log.Info($"_loadingLock Enter writelock, count: {_globalWritelockCountEnter}, ThreadId: {Thread.CurrentThread.ManagedThreadId}"); // deal with certain value _globalWritelockCountExit++; Log.Info($"_loadingLock exit writelock, count: {_globalWritelockCountExit}, ThreadId: {Thread.CurrentThread.ManagedThreadId}"); } } finally { _globalReadlockCountExit++; Log.Info($"_loadingLock Exit upgrade readlock, count: {_globalReadlockCountExit}, ThreadId: {Thread.CurrentThread.ManagedThreadId}"); } } }
2. 临时修复:同步化方法(若业务允许)
如果业务场景不需要异步执行,可移除方法的async修饰符,确保锁的获取与释放都在同一个线程上,彻底避免线程切换导致的锁状态异常。
3. 优化异常处理逻辑
移除catch块中直接抛出新异常的逻辑,保留原始异常信息,便于追踪锁状态异常的真实原因:
catch (Exception e) { Log.Error("Error during write lock operation", e); throw; // 抛出原始异常,不掩盖信息 }
4. 增加线程ID日志排查
在所有锁操作的日志中增加当前线程ID,确认获取锁与释放锁的线程是否一致,快速定位线程切换问题:
Log.Info($"_loadingLock Enter upgrade readlock, count: {_globalReadlockCountEnter}, ThreadId: {Thread.CurrentThread.ManagedThreadId}");
关键注意事项
- 所有包含
await的异步方法中,禁止使用ReaderWriterLockSlim、lock等线程亲和的同步锁,必须使用异步友好的锁机制。 IsXXXLockHeld仅用于调试断言,不能作为控制锁释放逻辑的依据,避免因线程切换导致的状态判断错误。
内容的提问来源于stack exchange,提问作者Eric MA
相关产品推荐
相关产品推荐

