lock 语句会被编译成 try-finally + Monitor.Enter/Exit
你写的
lock(obj) { ... } 不会原样保留,C# 编译器(csc)会在 IL 层面把它重写为显式的 Monitor.Enter和
Monitor.Exit调用,并包裹在
try...finally块中。这是为了确保即使临界区抛出异常,锁也一定会被释放。
关键点:
Monitor.Enter的返回值会被检查,如果为
false(表示未获取到锁),则不会进入
try块,而是直接跳过整个临界区逻辑(但这种情况极少发生,通常只出现在带超时的重载里) 标准
lock对应的是无超时版本的
Monitor.Enter(object),它不返回布尔值,所以编译器会用另一个重载:先调用
Monitor.Enter(object, ref bool)(.NET Core 2.0+ / .NET 5+ 默认行为),或在旧版中插入额外逻辑模拟原子性 编译后的
finally块里,
Monitor.Exit是无条件执行的,哪怕
Enter失败也不会进
finally
看一个具体反编译例子
假设你有这段 C# 代码:
object _lock = new object();
lock (_lock)
{
Console.WriteLine("in critical section");
}用
ildasm或
dotnet ilc查看其 Release 模式编译后的 IL(简化后)大致如下:
.try
{
IL_0000: ldarg.0
IL_0001: ldfld object Test::<_lock>k__BackingField
IL_0006: stloc.0
IL_0007: ldloc.0
IL_0008: call void [System.Runtime]System.Threading.Monitor::Enter(object)
IL_000d: nop
IL_000e: ldstr "in critical section"
IL_0013: call void [System.Console]System.Console::WriteLine(string)
IL_0018: nop
IL_0019: leave.s IL_0025
} // end .try
finally
{
IL_001b: ldloc.0
IL_001c: call void [System.Runtime]System.Threading.Monitor::Exit(object)
IL_0021: nop
IL_0022: endfinally
} // end handler注意:
leave.s是跳转到
finally之后的指令,不是跳过
finally—— 这正是保证
Exit总被执行的关键。
为什么不能手动写 Monitor.Enter/Exit 替代 lock
看似等价,但手动写容易出错:
漏掉try-finally,导致异常时锁不释放 → 死锁风险 在
Enter后、
try前就抛异常 →
finally还没建立,
Exit根本不会执行 误用
Monitor.TryEnter但忘记判断返回值,直接进临界区 → 逻辑错误 .NET 6+ 中,
lock还可能被 JIT 优化为轻量级锁(如偏向锁、自旋锁)路径,而手写调用绕过了这些优化层
lock 编译行为在不同 .NET 版本有差异
主要区别在锁获取的“原子性保障”实现方式:
.NET Framework 4.8 及更早:使用Monitor.Enter(object)+ 隐式异常处理模拟成功标志 .NET Core 2.0+ / .NET 5+:默认改用
Monitor.Enter(object, ref bool),由运行时保证调用本身是原子的,避免竞态条件 所有版本都强制生成
try-finally结构,这点没有例外
如果你在反编译时看到
ref bool参数和两次
ldloca.s指令,说明你正面对的是较新运行时的编译输出。这个细节常被忽略,但它直接影响锁失败时的行为可预测性。
