lock不是万能锁,仅适用于极短、不阻塞、不抛异常的临界区;必须用private readonly object或static readonly object作锁对象,禁用this、字符串、值类型;不可在lock内await或执行I/O;应优先用Interlocked、SemaphoreSlim等异步友好原语。

lock 不是万能锁,用错比不用更危险。 它只适合保护极短、确定不会阻塞、不抛异常(或已妥善处理)的临界区。多数人踩坑不是因为不会写 lock,而是没想清楚该不该用、锁什么、锁多久。
锁对象必须是私有 readonly 引用类型
常见错误是锁 this、"myLock"、typeof(MyClass) 或值类型变量。这些要么导致死锁,要么根本不起作用。
-
lock(this):外部代码也能lock(obj),协作完全失控 -
lock("abc"):字符串驻留机制会让所有同字面量的锁指向同一对象,跨类跨模块互相干扰 -
lock(123):值类型会装箱,每次生成新对象,锁形同虚设 -
lock(_lockObj)必须是private static readonly object _lockObj = new object();(静态共享资源)或private readonly object _lockObj = new object();(实例级资源)
lock 块里不能 await,也不能做 I/O 或耗时操作
一旦在 lock 里调用 await,编译直接报错;若强行用同步等待(如 .Wait()),线程卡死、死锁、吞异常全来了。
- 错误写法:
lock(_lockObj) { await File.ReadAllTextAsync(path); }—— 编译不过 - 更隐蔽的错误:
lock(_lockObj) { httpClient.GetAsync(url).Wait(); }—— 线程池饥饿,后续请求全堵住 - 正确替代:
SemaphoreSlim(初始化new SemaphoreSlim(1, 1)),配合await _sem.WaitAsync()+try/finally _sem.Release() - 如果只是原子计数,优先用
Interlocked.Increment(ref _count),零锁开销
锁的范围必须包住所有共享访问路径
现象是“偶尔报 Collection was modified”或数据错乱,本质是锁没盖住完整读-改-写周期。
- 错:先
foreach遍历集合,再lock里删元素 —— 遍历本身已暴露在并发下 - 对:整个遍历+修改逻辑都放进
lock块里,且避免在锁内调用虚方法、事件或委托(它们可能重入并再次尝试加锁) - 特别注意:多个共享字段(如
_cache和_lastUpdated)必须共用同一把锁,不能一个用_lockA,一个用_lockB - 嵌套锁务必固定顺序:始终先锁
_lockA再锁_lockB,否则线程 A(A→B)和线程 B(B→A)必然死锁
.NET 9+ 推荐用 System.Threading.Lock 类型
这不是语法糖,而是性能优化的原生锁类型。编译器能识别它并生成更轻量的指令(using(x.EnterScope())),避免传统 Monitor 的堆分配和上下文切换开销。
- 写法:
private static readonly Lock _lock = new Lock();,然后lock(_lock) { ... } - 旧版 .NET 仍用
object,但别混用:不要把Lock实例转成object再 lock,编译器会警告 - 别为省一行代码复用锁对象——不同业务逻辑用同一把锁,等于人为串行化,吞掉所有并发收益
最常被忽略的一点:lock 解决的是「多线程同时改同一块内存」的问题,不是「怎么让异步操作变线程安全」。后者得靠 async-aware 同步原语,或者重构为无状态、不可变、消息驱动的设计。











