基于customerNo限制API并行调用:异步场景Monitor使用异常问题
问题背景
需求:针对查询参数customerNo,同一参数值的API调用不允许并行执行,不同参数值的调用需支持并行。
使用await client.GetAsync(QueryHelpers.AddQueryString(path, parameters))进行异步API调用时,尝试用System.Threading.Monitor.TryEnter加锁,释放锁时抛出异常:
System.Threading.SynchronizationLockException: Object synchronization method was called from an unsynchronized block of code.
相关代码片段:
try { Monitor.TryEnter(lockObj, timeout, ref lockTaken); if (lockTaken) { List<PatientData> patients = await RetrievePatientsDataAsync(client, new Dictionary<string, string> { ["customerNo"] = customerNo, ["offset"] = _lindeAPIOptions.Offset, ["pagesize"] = _lindeAPIOptions.Pagesize }); data.Patients = patients; return data; } } finally { // Ensure that the lock is released. if (lockTaken) { Monitor.Exit(lockObj); } }
其中RetrievePatientsDataAsync内部包含上述异步API调用。
异常原因
Monitor是线程绑定的同步原语:获取锁的线程必须是释放锁的线程。但异步代码中,await会导致线程切换——执行await前的代码在一个线程,await完成后可能在另一个线程继续执行。这就导致Monitor.Exit时,当前线程并非获取锁的线程,触发SynchronizationLockException。
解决方案:基于SemaphoreSlim的按参数锁
我们需要一个异步友好的锁机制,并且能按customerNo区分锁。可以用Dictionary<string, SemaphoreSlim>来为每个customerNo维护独立的信号量,实现同一customerNo串行、不同customerNo并行的效果。
步骤1:定义全局的信号量字典
private static readonly Dictionary<string, SemaphoreSlim> _customerLocks = new Dictionary<string, SemaphoreSlim>(); private static readonly object _dictionaryLock = new object();
_dictionaryLock用于保护对_customerLocks的并发访问,避免字典操作的线程安全问题。
步骤2:修改业务逻辑代码
// 获取当前customerNo对应的信号量 SemaphoreSlim semaphore; lock (_dictionaryLock) { if (!_customerLocks.TryGetValue(customerNo, out semaphore)) { semaphore = new SemaphoreSlim(1, 1); // 初始计数1,最大计数1,实现互斥 _customerLocks.Add(customerNo, semaphore); } } try { await semaphore.WaitAsync(timeout); // 异步等待获取锁 List<PatientData> patients = await RetrievePatientsDataAsync(client, new Dictionary<string, string> { ["customerNo"] = customerNo, ["offset"] = _lindeAPIOptions.Offset, ["pagesize"] = _lindeAPIOptions.Pagesize }); data.Patients = patients; return data; } finally { semaphore.Release(); // 释放信号量 // 可选:清理长时间未使用的信号量,避免内存泄漏 lock (_dictionaryLock) { if (semaphore.CurrentCount == 1) // 当前信号量未被占用 { _customerLocks.Remove(customerNo); semaphore.Dispose(); } } }
方案说明
SemaphoreSlim.WaitAsync是异步方法,支持await,不会阻塞线程,且不绑定特定线程,完美适配异步场景。- 每个
customerNo对应一个独立的SemaphoreSlim,保证同一customerNo的调用串行执行,不同customerNo的调用互不干扰,支持并行。 - 可选的清理逻辑:当信号量回到未占用状态(
CurrentCount == 1)时,从字典中移除并释放,避免内存泄漏。
内容的提问来源于stack exchange,提问作者GIovanni

