惊喜:你可以‘拦截’C# lock 语句

发布: (2026年2月18日 GMT+8 06:09)
7 分钟阅读
原文: Dev.to

Source: Dev.to

封面图片:“惊喜:你可以‘拦截’C#的 lock 语句”

Dmitry Dorogoy

你可以在 C# 中“劫持” lock。下面是它的工作原理,以及为什么 不应该这么做。

很多开发者认为 lock 是某种运行时的“魔法”。其实并不是。在大多数情况下,它只是语法糖:编译器会把它重写为对 System.Threading.Monitor 的调用。

这种重写存在一个丑陋的边缘情况。如果你的项目定义了自己的 System.Threading.Monitor,编译器可能会绑定到你的类型,而不是 BCL 中的那个。换句话说,你可以改变 lock 的含义。

这是一种派对技巧,也是一把脚枪。请把它当作警示故事,而 不是一种技术手段。

注意:.NET 9C# 13 开始,当表达式是 System.Threading.Lock 时,lock 有一个特殊的快速路径。在这种情况下,它会编译为 using (x.EnterScope()) { … },而不是 Monitor.Enter/Exit
下面展示的劫持方式适用于经典的 lock (object) 路径。

编译器为 lock 生成的代码

对于经典的 lock (obj) { … } 情形,C# 编译器会生成等价于以下代码的实现:

object _lockObj = obj;
bool _lockWasTaken = false;
try
{
    System.Threading.Monitor.Enter(_lockObj, ref _lockWasTaken);
    // Your code...
}
finally
{
    if (_lockWasTaken) System.Threading.Monitor.Exit(_lockObj);
}

因此 lock 在运行时并不是“特殊”的,它是一种编译器模式,会展开为已知的调用。

劫持技巧

如果你定义一个与 BCL 类型完全限定名相同的类型,就可以让编译器调用 你的 方法。

最小示例(LINQPad 代码片段):

using System;

class Program
{
    static readonly object _sync = new();

    static void Main()
    {
        // 看起来像普通的 lock,但实际上被劫持了。
        lock (_sync)
        {
            Console.WriteLine("Working inside the lock...");
        }
    }
}

// 我们“伪造” System 命名空间和类。
namespace System.Threading
{
    public static class Monitor
    {
        public static void Enter(object obj, ref bool lockTaken)
        {
            Console.WriteLine("Hijacked: Enter() was called");
            lockTaken = true; // 重要:否则 Exit() 不会被调用。
        }

        public static void Exit(object obj)
        {
            Console.WriteLine("Hijacked: Exit() was called");
        }
    }
}

运行它,你会看到 Hijacked: 的信息。这就证明编译器将 lock 绑定到了 你的 System.Threading.Monitor

为什么会这样?

因为名称解析发生在 编译时

你的编译单元中包含一个 System.Threading.Monitor 类型,同时在引用的程序集里也有一个。编译器看到这两个类型并选择其中一个。如果它选择了你的类型,重写仍然会发生,但它会针对你的方法。

这正是编译器警告想要告诉你的。

不要忽视警告

这段代码应该会产生 CS0436。在这种情况下,它并不是“噪声”;它正是重点所在。

CS0436 的含义是:

  • 你的编译单元中的类型与导入的类型之间存在冲突
  • 编译器将使用来自你的编译单元的类型

如果你自定义的 Enter 方法实际上并没有加锁,那么多个线程可能会同时进入所谓的临界区。这会破坏不变量,导致最糟糕的错误:难以复现且在调试器下会消失的罕见竞争。

这种情况也可能意外发生:

  • 一个辅助类命名为 Monitor,却位于错误的命名空间
  • 某个依赖项提供了冲突的类型
  • 全局抑制了警告,导致 CS0436 被忽略

如果你在 System.Threading.Monitor 周围看到 CS0436,请立即停下来进行调查。

更安全的目标:测量锁争用

如果你的目标是可观测性,而不是破坏,那么有一种更好的方法:在不触碰 lock 的情况下测量争用。

.NET 为监视器争用提供了运行时事件:

  • ContentionStart_V2 – 事件 ID 81
  • ContentionStop_V1 – 事件 ID 91(包含 DurationNs
  • 关键字是 ContentionKeyword0x4000

你可以通过 EventListener 监听这些事件。

(LINQPad 代码片段):

using System;
using System.Diagnostics.Tracing;
using System.Threading;

sealed class LockMonitor : EventListener
{
    public long TotalWaitTimeNs;
    int _durationIndex = -1;

    protected override void OnEventSourceCreated(EventSource source)
    {
        if (source.Name == "Microsoft-Windows-DotNETRuntime")
        {
            EnableEvents(source, EventLevel.Informational, (EventKeywords)0x4000);
        }
    }

    protected override void OnEventWritten(EventWrittenEventArgs eventData)
    {
        // ContentionStop_V1
        if (eventData.EventId != 91 || eventData.PayloadNames is null)
            return;

        if (_durationIndex < 0)
            _durationIndex = eventData.PayloadNames.IndexOf("DurationNs");

        if (_durationIndex < 0)
            return;

        // DurationNs is documented as a Double. Convert and keep nanoseconds as a long for easy accumulation.
        var durationNs = (long)Convert.ToDouble(eventData.Payload![_durationIndex]!);
        Interlocked.Add(ref TotalWaitTimeNs, durationNs);
    }
}

上面的代码片段已故意截断,以匹配原始来源。

使用此方法可以获得的内容

  • lock 仍然是一个真实的锁。
  • 您可以收集线程等待的时间。
  • 您可以比较“好”和“坏”的代码路径,清晰地看到争用情况。

如果需要更深入的报告(每个锁对象、堆栈跟踪、时间线),请使用 dotnet‑tracePerfView 或基于 ETW/EventPipe 的分析器。上面的 EventListener 示例是一个很好的最小演示。

要点

  • lock 是编译器糖,通常会展开为 System.Threading.Monitor.Enter/Exit
  • 类型名称冲突可能会改变编译器生成的代码。CS0436 警告你正在改变语义。
  • 劫持 lock 是一个很巧妙的演示,但在实际代码中是极其糟糕的做法。
  • 在诊断时,建议使用争用事件和合适的工具,而不是使用技巧。
0 浏览
Back to Blog

相关文章

阅读更多 »

学习 C# 的第 -1 天

入门 今天,我正式开始了我的 C 之旅——不是从高级主题,而是从最基础的内容开始。在构建复杂的应用程序之前,我想…

Windows Signal 的补充阅读

Signal 设置中断信号处理。语法 c void __cdecl signal(int sig, int func(int), int); 参数 - sig – 信号值。 - func – 指向函数的指针。