如何使用ConcurrentQueue与lock实现C# API请求按顺序执行锁操作
两种有序锁方案乱序根因定位
- 自定义QueuedFunctions
方案问题
核心问题是入队、抢锁、出队执行三个操作拆分导致的逻辑误解。ConcurrentQueue仅能保证入队操作本身的线程安全,无法保证「线程A先发起Execute调用 → 线程A的委托先入队」的顺序和你预期的请求提交顺序完全匹配:- 并发场景下线程调度顺序不可控,后发起调用的线程可能先完成入队操作
- 若你单元测试中用线程ID、或闭包捕获的循环变量作为顺序判断依据,大概率是测试代码本身的逻辑错误,而非锁实现的执行顺序错误
- 大量线程同时抢内部锁会导致严重的上下文切换开销,是触发超时的核心原因
- 票号排队锁方案问题
票号逻辑本身是FIFO有序锁的标准实现,出现乱序90%以上是实现细节错误:- 待执行票号更新时机错误:必须在当前委托执行完成、释放锁之前更新待执行票号,而非进入临界区时更新
- 唤醒逻辑错误:释放锁时只需调用
Monitor.Pulse()唤醒下一个等待线程即可,调用Monitor.PulseAll()会导致大量线程被唤醒后抢锁、再判断票号不匹配重新进入等待,额外增加开销,极端场景下可能因为线程调度导致看起来乱序的假象 - 同样存在测试代码逻辑错误的可能性,比如用线程启动顺序代替票号生成顺序作为判断依据
可用FIFO有序锁实现
以下为标准的票号排队锁实现,兼顾有序性和并发性能:
public class QueuedLock : IDisposable { private readonly object _lockObj = new(); private long _currentTicket = 0; private long _nextTicket = 0; public void Enter() { // 原子生成票号,保证票号分配顺序和请求进入顺序一致 long myTicket = Interlocked.Increment(ref _nextTicket); lock (_lockObj) { while (myTicket != _currentTicket) { // 票号不匹配则等待 Monitor.Wait(_lockObj); } } } public void Exit() { lock (_lockObj) { // 先更新待执行票号,再唤醒下一个线程 Interlocked.Increment(ref _currentTicket); Monitor.Pulse(_lockObj); } } public void Dispose() { // 此处可加等待所有排队任务完成的逻辑,按需实现 } // 配合using语法糖使用的封装 public IDisposable Lock() { Enter(); return new Releaser(this); } private struct Releaser : IDisposable { private readonly QueuedLock _owner; public Releaser(QueuedLock owner) => _owner = owner; public void Dispose() => _owner.Exit(); } }
使用示例
private static readonly QueuedLock _queuedLock = new(); public T ProcessData<T>(Func<T> func) { using (_queuedLock.Lock()) { return func(); } }
高并发场景优化建议
- 若排队等待过长触发超时,可在
Enter方法中增加超时判断:调用Monitor.Wait(_lockObj, 超时时间),超时后抛出异常或返回降级结果,避免请求堆积 - 若临界区执行逻辑较短,可替换为自旋等待替代
Monitor.Wait,减少线程切换开销,进一步提升性能 - 单元测试时需在生成票号/入队时就记录顺序标识,写入委托的执行日志中,禁止用线程ID、线程启动顺序作为执行顺序的判断依据
内容的提问来源于stack exchange,提问作者dragnash
相关产品推荐
相关产品推荐

