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

时间:2018-10-26 22:56:34

标签: c# constructor static clr jit

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

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

要清楚,我想知道规范的实现是如何实现的。我知道静态构造函数是线程安全的,这个问题并不是这样。它询问该实现如何确保这一点,以及它是否在内部使用锁和检查(这些锁不是c Sharp中的锁,而是JIT / CLR /其他实现使用的锁)。

2 个答案:

答案 0 :(得分:1)

  

是否每次访问静态字段都输入锁,请检查静态   构造函数是初始化的,如果不是,则将其初始化?

我怀疑它是否本身会被锁定,我想CLR只是确保以唯一的方式对IL进行排序,尽管老实说我不太确定。

  

这不会很慢吗?

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静态构造函数

  

封闭类的静态构造函数最多在一个类中执行一次   给定的应用程序域。静态构造函数的执行是   由以下事件中的第一个触发,发生在   应用程序域:

     
      
  • 已创建该类的实例。
  •   
  • 该类的任何静态成员都被引用。
  •   
     

...

     

因为静态构造函数对每个对象执行一次   封闭构造的类类型,这是一个方便执行的地方   对无法在以下位置检查的type参数进行运行时检查   通过约束(第15.2.5节)进行编译。

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

最后,由于浏览规范充满乐趣和欢喜(各个结果可能会有所不同),因此您可能会遇到更多奇怪的情况,例如建立循环依赖关系

  

可以构造允许静态的循环依赖   具有变量初始值设定项的字段将在其默认值中被观察   价值状态。

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);
   }
}
     

产生输出

X = 1, Y = 2
     

要执行Main方法,系统首先运行的初始化程序   B.Y,在B类的静态构造函数之前。 Y的初始化器导致A的   因为引用了A.X的值,所以要运行的静态构造函数。

     

A的静态构造函数将继续计算A的值   X,然后获取默认值Y,即零。斧头   因此被初始化为1。运行A的静态字段的过程   初始化程序和静态构造函数随后完成,返回到   Y的初始值的计算,结果为2。

答案 1 :(得分:1)

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

首次调用方法时,将调用该方法的临时入口点,该入口点主要负责JITing方法的IL代码。临时入口点(特别是prestub)检查所调用方法类型的静态构造方法的类型(无论该方法是否为static实例)。如果是Precise,则临时入口点可确保已执行该类型的静态构造函数。

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

静态构造函数是通过调用CheckRunClassInitThrowing来执行的。此函数基本上检查类型是否已经初始化,如果没有,则调用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.
    }

    .
    .
    .

}

与应用程序域无关的类型和NGEN'ed类型的静态构造函数的处理方式不同。此外,出于性能原因,CoreCLR实现未严格遵守Precise构造函数的语义。有关更多信息,请参阅corinfo.h顶部的注释。