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

.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.

问题根源分析

  1. 异步方法与同步锁的线程亲和冲突:ReaderWriterLockSlim是线程亲和锁,锁的持有与释放必须在同一个线程上。但当前方法是async Task,await操作会导致线程切换,后续释放锁的线程可能不是获取锁的线程,直接破坏锁的状态,引发未持有锁却释放的异常——这是最核心的原因。
  2. 异常处理的掩盖问题:获取WriteLock的catch块直接抛出新异常,可能掩盖原始锁状态异常,导致无法追踪锁状态异常的真实触发点。
  3. 超时后的锁状态不确定性: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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.15 15:34:51