【C#】 lock 关键字

在 C# 里,lock 关键字就是对 Monitor.Enter/Exit 的简写。它的作用是保证"同一时刻只有一个线程能进入被保护的代码块",从而避免多个线程同时修改同一个共享状态导致竞态条件(race condition)。


一、结合Jog 的例子讲解

csharp 复制代码
// MotionService 内部,用于保护 _isJogging 标志位
private static volatile bool _isJogging = false;
private static readonly object _jogLock = new object();

public static void JogStart(...)
{
    // 1. 用 lock 把后面的检查和赋值变成"原子"操作
    lock (_jogLock)
    {
        if (_isJogging)
            return;      // 已经有线程在 Jog,就不再启动新的
        _isJogging = true; // 否则立刻把标志置为 true
    }

    // ... 后面启动后台循环的逻辑 ...
}
  • 为什么要 lock?

    假设有两个线程几乎同时调用 JogStart(),如果没有 lock,它们都可能先执行 if (_isJogging)(此时还是 false),然后同时进入,然后又同时把 _isJogging = true,最后就启动了两条后台循环,违反了"同一时刻只有一个 Jog" 的设计初衷。

  • lock (_jogLock) 做了什么?

    1. 某线程执行到 lock 时,会尝试"拿到" _jogLock 的内部锁(Monitor)
    2. 如果别的线程已拿到,就阻塞等待 ,直到那线程执行完 lock 块、离开后释放锁
    3. 拿到锁后,该线程才能进入大括号内,执行检查和赋值
    4. 结束 } 时自动释放锁,其他线程才有机会进入

这样,你就把"检查标志"+"设置标志"整个过程当成一个不可分割的操作,彻底杜绝并发竞态。


二、再举一个常见例子:线程安全的银行账户

假设有一个 BankAccount 类,多个线程可能同时给同一个账户存取款。我们要保证余额永远不会因为并发而乱掉,就可以用 lock

csharp 复制代码
public class BankAccount
{
    private decimal _balance = 0m;
    private readonly object _balanceLock = new object();

    public void Deposit(decimal amount)
    {
        // 存款操作必须互斥
        lock (_balanceLock)
        {
            _balance += amount;
        }
    }

    public void Withdraw(decimal amount)
    {
        // 取款操作也必须互斥,并在余额足够时才扣款
        lock (_balanceLock)
        {
            if (_balance < amount)
                throw new InvalidOperationException("余额不足");
            _balance -= amount;
        }
    }

    public decimal GetBalance()
    {
        // 如果你也想在读余额时保证最新一致,可以加锁;否则可直接返回
        lock (_balanceLock)
        {
            return _balance;
        }
    }
}
  • _balanceLock :保护 decimal _balance 的私有对象
  • 同一时刻,只能有一个线程在执行 DepositWithdraw 中被 lock 包围的部分
  • 如果两个线程同时存取,第二个会在 lock 阻塞,等第一个操作完成才继续

总结

  • lock(obj) { ... }:等同于

    csharp 复制代码
    Monitor.Enter(obj);
    try { ... }
    finally { Monitor.Exit(obj); }
  • 使用原则

    1. 用私有的 readonly 对象做锁,不要 lock(this)lock(typeof(...))
    2. 把所有访问共享状态(变量/集合/字段)的代码都包在 lock
    3. 尽量让锁内代码简短,避免长时间占用锁导致其他线程饥饿
  • 效果:保证多线程环境下对共享数据的"检查-修改"操作是原子的,消除竞态,确保程序行为可预测、不会乱跑。

在 C# 里,lock 语句后面必须跟一个 引用类型的"同步对象" (sync object),它的作用就是充当「看门人」:任何线程在进入 lock(obj){ ... } 这一段代码前,都要先尝试"拿到"这个对象的监视器(Monitor);如果已经被别的线程拿走,就会在这里阻塞,直到对方执行完 lock 块、释放锁。


补充

一、为什么要 private static readonly object _jogLock = new object();

  1. 专门的"锁"对象

    • 你要给 lock 一个「值唯一且不会被外部改动」的对象来做锁。
    • new object() 生成一个全新的、空白的对象实例,除了用来锁,它不会被当成其它用途也不会被别的代码意外取锁。
  2. private

    • 锁对象对外不可见,避免外部其他代码也去锁它,减少死锁风险。
  3. static

    • 因为 MotionService 中所有成员(如 _isJoggingJogStart)都是静态的,所以锁也必须静态的,才能跨所有调用者、所有线程保护同一块共享状态。
  4. readonly

    • 一旦初始化后,这个引用永远指向同一个对象,保证锁的一致性;如果别人把它指向别的对象,就可能拿不到原来的锁。
csharp 复制代码
// 定义一把专用的"锁"
private static readonly object _jogLock = new object();

二、为什么用 lock(_jogLock) 而不是 lock(this) 或者锁字符串?

  • 锁 "this" 有风险

    • 如果别人也 lock(someInstance),就可能和你无意中互相等待;而且外部很容易拿到 this,耦合度高。
  • 锁字符串或 Type 对象更危险

    • 字符串常量会被 CLR 统一(interning),多处同名字符串可能共用同一个锁,容易引发意外死锁;
    • lock(typeof(SomeClass)) 同理,会和任何锁这个 Type 的代码互相影响。
  • 最佳实践

    • 总是为每个需要保护的"共享资源"声明一个 私有的、专用的、不可被外部访问的 readonly object,只在内部用它做 lock

三、lock(_jogLock) 的设计目的

csharp 复制代码
public static void JogStart(...)
{
    lock (_jogLock)
    {
        if (_isJogging) return;   // ※ 原子检查:  
        _isJogging = true;        //   先检查、再设置,都在同一把锁里,一次性完成
    }
    // ... 启动后台 Jog 逻辑 ...
}
  • 原子性 :把「看 _isJogging 标志」和「写 _isJogging = true」这两步放在同一个锁里,绝不被其它线程打断。
  • 竞态保护 :任何时候只有拿到 _jogLock 的线程才可能进入这段代码,第二个线程会被挂起在 lock 处,等到第一个线程释放锁后再来检测 _isJogging,确保"同一时刻"最多只有一个线程把标志从 false 变成 true

四、再举一个例子:线程安全的计数器

csharp 复制代码
public class SafeCounter
{
    private int _count = 0;
    private readonly object _countLock = new object();

    public void Increment()
    {
        lock (_countLock)
        {
            // 下面两步必须原子进行,不能被其他线程同时执行
            _count = _count + 1;
        }
    }

    public int GetValue()
    {
        lock (_countLock)
        {
            return _count;
        }
    }
}
  • _countLock 就是专门保护 _count 的锁。
  • Increment() 在加 1 前先拿锁,确保两个线程不会同时读取旧值并写回相同的新值。
  • GetValue() 也可以加锁,确保读到的是最新且一致的值。

小结

  • 锁对象 :私有、专用、不变 (private readonly object _lock = new object())

  • lock(obj){...}:把一段关键代码变成「同一时间只有一个线程能进」

  • 设计原则

    1. 不锁 this、不锁字符串、不锁 Type。
    2. 每个类/资源用自己的私有锁对象。
    3. 锁范围要尽可能小,只包围需要原子执行的那几行。
相关推荐
zhangfeng11331 小时前
openclaw skills 小龙虾技能 通讯仿真 matlab skill Simulink Agentic Toolkit,通过kimi找到,mcp通讯
开发语言·matlab·openclaw·通讯仿真
Javatutouhouduan7 小时前
2026Java面试的正确打开方式!
java·高并发·java面试·java面试题·后端开发·java编程·java八股文
chao1898447 小时前
基于 SPEA2 的多目标优化算法 MATLAB 实现
开发语言·算法·matlab
JAVA面经实录9177 小时前
Java初级最终完整版学习路线图
java·spring·eclipse·maven
赏金术士8 小时前
Kotlin 习题集 · 高级篇
android·开发语言·kotlin
Cat_Rocky8 小时前
k8s-持久化存储,粗浅学习
java·学习·kubernetes
楼兰公子9 小时前
buildroot 在编译rust时裁剪平台类型数量的方法
开发语言·后端·rust
知识领航员9 小时前
蘑兔AI音乐深度实测:功能拆解、实测表现与适用场景
java·c语言·c++·人工智能·python·算法·github
吴声子夜歌9 小时前
Go——并发编程
开发语言·后端·golang
释怀°Believe9 小时前
Spring解析
java·后端·spring