【C#】 lock 关键字

发布于:2025-05-18 ⋅ 阅读:(17) ⋅ 点赞:(0)

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


一、结合Jog 的例子讲解

// 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

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) { ... }:等同于

    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

    • 一旦初始化后,这个引用永远指向同一个对象,保证锁的一致性;如果别人把它指向别的对象,就可能拿不到原来的锁。
// 定义一把专用的“锁”
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) 的设计目的

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

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

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. 锁范围要尽可能小,只包围需要原子执行的那几行。

网站公告

今日签到

点亮在社区的每一天
去签到