当前位置: 首页 > ds >正文

【C#】 lock 关键字

在 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. 锁范围要尽可能小,只包围需要原子执行的那几行。
http://www.xdnf.cn/news/6840.html

相关文章:

  • 【笔记】导出Conda环境依赖以复现项目虚拟环境
  • 深度学习驱动下的目标检测技术:原理、算法与应用创新(二)
  • LLM学习笔记(七)注意力机制
  • C# NX二次开发-实体离散成点
  • 使用pyinstaller生成exe时,如何指定生成文件名字
  • Linux!启动~
  • WHAT - 前端同构 Isomorphic Javascript
  • Ubuntu系统安装VsCode
  • UAI 2025重磅揭晓:录取数据公布(附往届数据)
  • Python字符串常用内置函数详解
  • 独立开发者利用AI工具快速制作产品MVP
  • Qt功能区:Ribbon使用
  • Linux复习笔记(六)shell编程
  • 实现书签-第一部分
  • 中大型水闸安全监测系统建设实施方案
  • 在服务器上安装AlphaFold2遇到的问题(2)
  • 【C++】 —— 笔试刷题day_30
  • 【C++ | 内存管理】C++ weak_ptr 详解:成员函数、使用例子与实现原理
  • 力扣654题:最大二叉树(递归)
  • 实时技术方案对比:SSE vs WebSocket vs Long Polling
  • Java Set系列集合详解:HashSet、LinkedHashSet、TreeSet底层原理与使用场景
  • 产品经理入门——认识产品经理
  • OCCT知识笔记之Poly_Triangulation详解
  • YOLOv7训练时4个类别只出2个类别
  • vue使用Fabric和pdfjs完成合同签章及批注
  • 第八节第三部分:认识枚举、枚举的作用和应用场景
  • DeepSearch:WebThinker开启AI搜索研究新纪元!
  • 游戏站的几种形式
  • redis数据结构-11(了解 Redis 持久性选项:RDB 和 AOF)
  • STM32H743IIT6_ADC采集误差分析与ADC_DMA