c# 在并发编程中,无锁和有锁算法的权衡

来源:这里教程网 时间:2026-02-21 17:37:14 作者:

无锁算法在 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
,压测发现瓶颈后再针对性替换为无锁实现——过早优化无锁,八成是在给自己埋坑。

相关推荐