如何为ConcurrentQueue入队元素无锁分配连续自增Id
问题场景
- 基于ASP.NET Core 6搭建的业务系统,当前使用
ConcurrentQueue<Result>存储特定活动产生的运行结果,整体运行稳定。 - 新增需求:为
Result类型新增long类型Id属性,逻辑类似数据库连续主键,要求元素入队时获取序列下一个值,最初设想取值规则为queue.Count + 1。 - 原有入队逻辑依赖
ConcurrentQueue内部的线程安全保障,不需要手动加锁;为实现Id分配,目前仅构思出两种需要加锁的方案:- 方案1:锁定
Enqueue()方法访问权限,先获取队列当前长度,将长度值+1作为序号赋值给待入队元素的Id属性,再执行入队操作。 - 方案2:锁定
Enqueue()方法访问权限,先将元素入队,再获取队列最新长度更新对应元素的Id,该方案的缺陷是Id更新完成前元素处于状态不一致的问题。
- 方案1:锁定
- 核心诉求:是否可以不对
Enqueue()方法加锁实现需求?是否存在更优实现方式?要求尽可能保留现有ConcurrentQueue的使用,不替换已验证稳定的组件。
最优实现方案
完全可以不加锁实现需求,且不需要替换ConcurrentQueue,核心思路是放弃用队列Count属性计算Id的逻辑,改用独立的原子计数器生成连续Id,性能、稳定性都远好于加锁方案。
方案细节
使用Interlocked.Increment实现CPU级别的原子自增维护序列号,全程无手动锁,线程安全,代码实现如下:
// 与队列同生命周期的原子序列号计数器,初始值为0 private long _resultIdSeed = 0; private readonly ConcurrentQueue<Result> _resultQueue = new ConcurrentQueue<Result>(); /// <summary> /// 结果入队方法 /// </summary> public void AddResult(Result item) { // 原子操作获取下一个全局连续Id,无锁、无重复、严格递增 item.Id = Interlocked.Increment(ref _resultIdSeed); // 直接调用ConcurrentQueue原生入队方法,不需要额外加锁 _resultQueue.Enqueue(item); }
方案优势
- 完全保留现有
ConcurrentQueue的使用逻辑,之前稳定运行的队列读写逻辑不需要做任何修改,没有组件替换带来的稳定性风险。 - 全程无重量级锁:
Interlocked.Increment是硬件层面实现的原子操作,性能开销远低于lock语句,高并发场景下的吞吐表现比两种加锁方案高一个量级以上。 - 不存在状态不一致问题:Id在元素入队前就完成赋值,入队后的元素所有属性都是完整可用的,不会出现方案二中元素入队后Id还未更新就被消费的问题。
- Id生成逻辑严谨:生成的Id是全局严格连续递增的,不会出现重复、乱序问题,和数据库自增主键的行为完全一致。
注意事项
你最初设想的
queue.Count + 1Id生成规则本身存在逻辑漏洞:只要业务中存在出队操作,后续生成的Id必然会和历史Id重复。举个实际场景:队列先后入队3条数据,按规则生成Id1、2、3,此时出队2条,队列剩余1个元素,下一次入队时取Count + 1得到2,直接和历史Id=2的元素重复,完全失去唯一标识的作用。就算业务中不存在出队操作,
ConcurrentQueue.Count在高并发场景下返回的是执行语句时刻的队列快照值,你拿到Count之后可能已经有其他线程完成了入队,就算加了锁也会因为Count的快照特性出现Id重复的问题。而原子计数器方案维护的序列号是精确的全局入队计数,只要Id赋值和入队操作是连续同步执行的,Id顺序和入队顺序完全对齐,不存在偏差。
内容的提问来源于stack exchange,提问作者lonix
相关产品推荐
相关产品推荐

