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

How does an implementation of the c sharp specification ensure static constructors are executed in a threadsafe manner?

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

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

明确地说,我想知道规范的实现是如何实现这一点的。我知道静态构造函数是线程安全的,这个问题不是在问这个。它询问实现如何确保这一点,以及它是否在幕后使用锁和检查(这些锁不是 c sharp 中的锁,而是 JIT/CLR/其他实现使用的锁)。

Does every access to a static field enter a lock, check if the static constructor is initalized, then initialize it if it isn't?

我怀疑它本身是否会锁定,我想 CLR 只是确保 IL 以其独有的方式被命令和发出,但老实说我不太确定。

Wouldn't this be very slow?

private static void Main(string[] args)
{
   var t1 = Task.Run(
      () =>
         {
            Console.WriteLine($"{DateTime.Now.TimeOfDay} here 1");
            var val = Test.Value;
            Console.WriteLine($"{DateTime.Now.TimeOfDay} here 1 complete");
            return val;
         });
   var t2 = Task.Run(
      () =>
         {
            Console.WriteLine($"{DateTime.Now.TimeOfDay} here 2");
            var val = Test.Value;
            Console.WriteLine($"{DateTime.Now.TimeOfDay} here 2 complete");
            return val;
         });
   Task.WaitAll(t2, t2);
}

public static class Test
{
   static Test()
   {
      Thread.Sleep(2000);
      Value = 1;
   }

   public static int Value { get; }
}

输出

09:24:24.3817636 here 2
09:24:24.3817636 here 1
09:24:26.3866223 here 2 complete
09:24:26.3866223 here 1 complete

您在这里看到的不仅是编写得非常糟糕的代码,其他线程还必须等待这些类型的恶作剧完成。所以是的,如果您选择它,它可能会很慢。


ECMA Specifications

15.12 静态构造函数

The static constructor for a closed class executes at most once in a given application domain. The execution of a static constructor is triggered by the first of the following events to occur within an application domain:

  • An instance of the class is created.
  • Any of the static members of the class are referenced.

...

Because the static constructor is executed exactly once for each closed constructed class type, it is a convenient place to enforce run-time checks on the type parameter that cannot be checked at compiletime via constraints (§15.2.5).

没有提及它如何实现排他性(正如您所期望的那样),因为它只是一个实现细节,但我们所知道的是它确实

最后,因为浏览规范充满乐趣和欢闹(每个结果可能会有所不同),您可以自己遇到更多奇怪的情况,例如循环依赖

It is possible to construct circular dependencies that allow static fields with variable initializers to be observed in their default value state.

class A
{
   public static int X;
   static A()
   {
      X = B.Y + 1;
   }
}
class B
{
   public static int Y = A.X + 1;
   static B() { }
   static void Main()
   {
      Console.WriteLine("X = {0}, Y = {1}", A.X, B.Y);
   }
}

produces the output

X = 1, Y = 2

To execute the Main method, the system first runs the initializer for B.Y, prior to class B's static constructor. Y's initializer causes A's static constructor to be run because the value of A.X is referenced.

The static constructor of A in turn proceeds to compute the value of X, and in doing so fetches the default value of Y, which is zero. A.X is thus initialized to 1. The process of running A's static field initializers and static constructor then completes, returning to the calculation of the initial value of Y, the result of which becomes 2.

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

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

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

静态构造函数是通过调用CheckRunClassInitThrowing. This function basically checks whether the type has already been initialized, and if not, it calls DoRunClassInitThrowing来执行的,也就是真正调用静态构造函数的那个​​。在调用静态构造函数之前,需要获取与该构造函数关联的锁。每种类型都有一个这样的锁。但是,这些锁是延迟创建的。也就是说,只有当一个类型的静态构造函数被调用时,才会为该类型创建一个锁。因此,每个应用程序域都需要动态维护一个锁列表,并且这个列表本身需要一个锁来保护。因此,调用静态构造函数涉及两个锁:特定于应用程序域的锁和特定于类型的锁。下面的代码展示了这两个锁是如何获取和释放的(一些评论是我的)。

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 中立类型和 NGEN 类型的静态构造函数的处理方式不同。此外,出于性能原因,CoreCLR 实现并不严格遵守 Precise 构造函数的语义。有关详细信息,请参阅 corinfo.h.

顶部的评论