17370845950

C# 线程同步方法 C#如何使用lock关键字
lock关键字必须作用于引用类型对象,不能直接用于int、bool等值类型;应使用私有只读object字段作锁,避免用this或公共成员;lock不解决锁外竞态、跨进程同步或async/await场景,后者需SemaphoreSlim等替代方案。

lock 关键字必须作用于引用类型对象

直接对 intbool 或值类型变量加 lock 会编译失败,因为 lock 要求表达式结果是引用类型。常见错误写法:

int counter = 0;
lock (counter) { /* 编译错误:不能将 int 用作 lock 表达式 */ }
正确做法是声明一个专用的私有只读对象字段:
private readonly obj

ect _lockObj = new object(); // ... lock (_lockObj) { counter++; }
别用 thistypeof(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
别试图用 Task.Run(() => { lock (...) { ... } }) 来“绕过”,这只会增加线程开销且不解决根本问题。