c# lock 语句的编译后代码是什么样的

来源:这里教程网 时间:2026-02-21 17:41:52 作者:

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
指令,说明你正面对的是较新运行时的编译输出。这个细节常被忽略,但它直接影响锁失败时的行为可预测性。

相关推荐