无锁算法在 C# 中真的不加锁吗?
“无锁”不是字面意义的完全不用锁,而是指不依赖
lock、
Mutex、
Monitor这类阻塞式同步原语;它靠
Interlocked系列方法(如
Interlocked.CompareExchange)和 CPU 提供的原子指令(CAS)实现线程安全。关键在于:线程不会因竞争而挂起,但可能自旋重试——这会吃 CPU,尤其在高争用时。 典型场景:高频读写计数器、单生产者单消费者队列、对象池的轻量级分配 常见误判:把
volatile当成线程安全手段——它只保证可见性,不保证原子性或顺序性 风险点:
Interlocked仅支持基础类型(
int、
long、
ref等),无法直接用于复杂对象状态更新
C# 中哪些并发集合默认用了无锁?
ConcurrentQueue<t></t>、
ConcurrentStack<t></t>和
ConcurrentBag<t></t>在 .NET Core 2.1+ 及 .NET 5+ 中大量使用无锁策略(尤其是前两者底层基于
Interlocked+ 分段数组),但不是“纯无锁”:它们在扩容、边界处理等少数路径仍会退化到细粒度锁(如
SpinLock或内部
lock)。 不要假设“Concurrent”前缀 = 全程无锁;查看源码可知
ConcurrentDictionary<tkey tvalue></tkey>内部用分段锁(
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;
<p>// ✅ 推荐:用 lock 保护多字段协同更新
public void UpdateValueAndStatus(int newValue, string newStatus)
{
lock (_sync)
{
_value = newValue;
_status = newStatus;
// 还可能触发事件、更新缓存……这些无法用单一 CAS 表达
}
}</p><p>// ❌ 避免:试图用 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,压测发现瓶颈后再针对性替换为无锁实现——过早优化无锁,八成是在给自己埋坑。
