无锁算法在C#中并非完全不加锁,而是不使用lock等阻塞原语,依赖Interlocked和CAS实现线程安全;线程不挂起但可能自旋耗CPU,适用于高频简单操作,复杂逻辑或协同更新应优先用lock。

无锁算法在 C# 中真的不加锁吗?
“无锁”不是字面意义的完全不用锁,而是指不依赖 lock、Mutex、Monitor 这类阻塞式同步原语;它靠 Interlocked 系列方法(如 Interlocked.CompareExchange)和 CPU 提供的原子指令(CAS)实现线程安全。关键在于:线程不会因竞争而挂起,但可能自旋重试——这会吃 CPU,尤其在高争用时。
- 典型场景:高频读写计数器、单生产者单消费者队列、对象池的轻量级分配
- 常见误判:把
volatile当成线程安全手段——它只保证可见性,不保证原子性或顺序性 - 风险点:
Interlocked仅支持基础类型(int、long、ref等),无法直接用于复杂对象状态更新
C# 中哪些并发集合默认用了无锁?
ConcurrentQueue、ConcurrentStack 和 ConcurrentBag 在 .NET Core 2.1+ 及 .NET 5+ 中大量使用无锁策略(尤其是前两者底层基于 Interlocked + 分段数组),但不是“纯无锁”:它们在扩容、边界处理等少数路径仍会退化到细粒度锁(如 SpinLock 或内部 lock)。
- 不要假设“Concurrent”前缀 = 全程无锁;查看源码可知
ConcurrentDictionary内部用分段锁(locks[]数组),属于有锁优化而非无锁 - 性能拐点明显:当线程数远超 CPU 核心数,或单个操作耗时变长(如含 I/O 或复杂计算),无锁自旋开销会迅速压倒收益
- 调试困难:无锁逻辑出错往往表现为偶发数据丢失或无限循环,堆栈里看不到阻塞点,难复现
什么时候该主动放弃无锁,改用 lock?
当你需要保护一段**非原子的多步逻辑**,或者涉及**多个共享变量的协同更新**,硬套无锁极易出错。例如:检查一个条件后再修改两个字段,CAS 无法一步完成这种“检查-执行”事务。
- 典型信号:你开始写嵌套的
while (true)+CompareExchange循环,并在里面做条件判断和分支赋值——这已偏离无锁初衷,且极易漏掉 ABA 问题 - 更稳妥的选择:
lock虽然阻塞,但在争用不激烈(如每秒几十次操作)、临界区极短( - 注意
lock对象粒度:避免锁住this或typeof(MyClass),优先用私有readonly object _sync = new();
private readonly object _sync = new(); private int _value; private string _status;// ✅ 推荐:用 lock 保护多字段协同更新 public void UpdateValueAndStatus(int newValue, string newStatus) { lock (_sync) { _value = newValue; _status = newStatus; // 还可能触发事件、更新缓存……这些无法用单一 CAS 表达 } }
// ❌ 避免:试图用 Interlocked 拆解多步逻辑(错误且不可靠) public void BadAttemptToUpdate() { while (true) { var oldVal = _value; var newVal = oldVal + 1; if (Interlocked.CompareExchange(ref _value, newVal, oldVal) == oldVal) { // 此时 _status 可能已被其他线程改写,状态不一致 _status = "updated"; // 这行不是原子的! break; } } }
无锁的真正门槛不在代码长度,而在对内存模型、CPU 缓存一致性协议(如 MESI)和 ABA 本质的理解。多数业务场景下,先用 Concurrent* 集合或 lock,压测发现瓶颈后再针对性替换为无锁实现——过早优化无锁,八成是在给自己埋坑。











