lock 关键字必须作用于引用类型对象
直接对
int、
bool或值类型变量加
lock会编译失败,因为
lock要求表达式结果是引用类型。常见错误写法:
int counter = 0;
lock (counter) { /* 编译错误:不能将 int 用作 lock 表达式 */ }正确做法是声明一个专用的私有只读对象字段:private readonly object _lockObj = new object();
// ...
lock (_lockObj) { counter++; }别用 this、
typeof(MyClass)或公共字段——它们可能被外部代码锁定,导致死锁或意外阻塞。
lock 不是万能的,它只保证临界区串行执行
lock只确保同一把锁对象保护的代码块不会被多个线程同时进入,但它不解决以下问题: 锁外的共享变量读写仍可能引发竞态(比如先判断再修改的“检查-执行”逻辑) 锁粒度太粗会严重拖慢吞吐;太细则容易漏锁或重复锁 无法跨进程同步,也不能替代异步等待(
await内部不能用
lock) 例如下面这段看似安全的代码实际有问题:
if (list.Count == 0) {
lock (_lockObj) {
if (list.Count == 0) { // 必须双重检查!
list.Add(item);
}
}
}漏掉内层判断,就可能在两次 Count读取之间被其他线程插入元素,导致重复添加。
lock 和 Monitor.Enter/Exit 的关系
lock (obj) { ... } 是语法糖,编译后等价于调用 Monitor.Enter(obj)和
Monitor.Exit(obj),并包裹在
try/finally中。这意味着: 即使临界区内抛出异常,锁也会被释放(这是
lock安全的核心) 手动调用
Monitor.Enter时若忘记配对
Monitor.Exit,会导致永久死锁
Monitor.TryEnter(obj, timeout)可实现带超时的获取锁,而
lock不支持 所以除非需要超时或条件等待(
Monitor.Wait/
Pulse),否则优先用
lock。
lock 与 async/await 不能共存
在
async方法里直接写
lock会编译报错:
public async Task DoWorkAsync() {
lock (_lockObj) { // ❌ CS1996:无法在异步方法中使用 lock 语句
await Task.Delay(100);
}
}原因是 lock依赖线程上下文连续性,而
await可能切换线程。替代方案包括: 用
SemaphoreSlim(注意用
await semaphore.WaitAsync()+
finally { semaphore.Release(); })
把需要同步的纯 CPU 操作抽离到同步方法中,在 await前/后调用 改用不可变数据结构或无锁并发集合(如
ConcurrentQueue<t></t>) 别试图用
Task.Run(() => { lock (...) { ... } }) 来“绕过”,这只会增加线程开销且不解决根本问题。 