首页 > 解决方案 > c sharp 规范的实现如何确保静态构造函数以线程安全的方式执行?

问题描述

c# 静态构造函数保证只执行一次。因此,如果我说有十个线程访问类 A 的成员,而 A 的静态构造函数还没有运行,而 A 的静态构造函数运行需要 10 秒,那么这些线程将阻塞 10 秒。

这对我来说似乎很神奇 - 这是如何在 JIT/CLR 中实现的?对静态字段的每次访问是否都会进入锁,检查静态构造函数是否已初始化,如果没有则初始化它?这不会很慢吗?

为了清楚起见,我想知道规范的实现如何实现这一点。我知道静态构造函数是线程安全的,这个问题不是在问那个。它询问实现如何确保这一点,以及它是否在后台使用锁和检查(这些锁不是 c 语言中的锁,而是 JIT/CLR/其他实现使用的锁)。

标签: c#constructorstaticclrjit

解决方案


让我们首先回顾一下不同种类的静态构造函数以及指定何时必须执行的规则。有两种静态构造函数:PreciseBeforeFieldInit。显式定义的静态构造函数是精确的。如果一个类在没有明确定义的静态构造函数的情况下初始化了静态字段,则托管语言编译器定义一个执行这些静态字段初始化的类。精确的构造函数必须在访问任何字段或调用该类型的任何方法之前执行。BeforeFieldInit 构造函数必须在第一次静态字段访问之前执行。现在我将讨论在 CoreCLR 和 CLR 中何时以及如何调用静态构造函数。

第一次调用方法时,会调用该方法的临时入口点,该入口点主要负责 JITing 方法的 IL 代码。临时入口点(特别是 prestub)检查被调用方法类型的静态构造函数的类型(不管该方法是否是静态实例)。如果是 Precise,则临时入口点确保该类型的静态构造函数已被执行。

临时入口点然后调用 JIT 编译器来发出方法的本机代码(因为它是第一次被调用)。JIT 编译器检查方法的 IL 是否包括对静态字段的访问。对于每个访问的静态字段,如果定义该静态字段的类型的静态构造函数是 BeforeFieldInit,则编译器确保该类型的静态构造函数已被执行。因此,该方法的本机代码不包括对静态构造函数的任何调用。否则,如果定义该静态字段的类型的静态构造函数是 Precise,则 JIT 编译器会在每次访问方法的本机代码中的静态字段之前注入对静态构造函数的调用。

静态构造函数通过调用CheckRunClassInitThrowing来执行。这个函数基本上是检查类型是否已经初始化,如果没有就调用DoRunClassInitThrowing,这是实际调用静态构造函数的那个​​。在调用静态构造函数之前,需要获取与该构造函数关联的锁。每种类型都有一个这样的锁。但是,这些锁是惰性创建的。也就是说,只有当一个类型的静态构造函数被调用时,才会为该类型创建一个锁。因此,需要为每个 appdomain 动态维护一个锁列表,并且该列表本身需要被锁保护。所以调用静态构造函数涉及到两个锁:一个appdomain-specific锁和一个type-specific锁。以下代码显示了如何获取和释放这两个锁(一些评论是我的)。

void MethodTable::DoRunClassInitThrowing()
{

    .
    .
    .

    ListLock *_pLock = pDomain->GetClassInitLock();

    // Acquire the appdomain lock.
    ListLockHolder pInitLock(_pLock);

    .
    .
    .

    // Take the lock
    {
        // Get the lock associated with the static constructor or create new a lock if one has not been created yet.
        ListLockEntryHolder pEntry(ListLockEntry::Find(pInitLock, this, description));

        ListLockEntryLockHolder pLock(pEntry, FALSE);

        // We have a list entry, we can release the global lock now
        pInitLock.Release();

        // Acquire the constructor lock.
        // Block if another thread has the lock.
        if (pLock.DeadlockAwareAcquire())
        {
            .
            .
            .
        }

        // The constructor lock gets released by calling the destructor of pEntry.
        // The compiler itself emits a call to the destructor at the end of the block
        // since pEntry is an automatic variable.
    }

    .
    .
    .

}

appdomain-neutral 类型和 NGEN 类型的静态构造函数的处理方式不同。此外,出于性能原因,CoreCLR 实现并不严格遵守 Precise 构造函数的语义。有关更多信息,请参阅corinfo.h顶部的注释。


推荐阅读