1

The c# static constructors is guaranteed to execute only once. Therefore, if I have say ten threads accessing a member of class A, and the static constructor of A hasn't been run, and the static constructor of A takes 10 seconds to run, these threads will block for ten seconds.

This seems amazing to me - how is this achieved within the 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? Wouldn't this be very slow?

To be clear, I want to know how an implementation of the specification achieves this. I know that static constructors are threadsafe, this question is not asking that. It is asking how the implementation ensures this, and whether it uses locks and checks under the hood (these locks are not locks in c sharp, rather locks used by the JIT/CLR/other implementation).

Nick
  • 920
  • 1
  • 7
  • 21
  • This is not a duplicate - that question asks if it's threadsafe, I want to know WHY it's threadsafe. – Nick Oct 27 '18 at 11:02
  • I reopened this, maybe someone else can give a better description as to whats going on internally. – TheGeneral Oct 27 '18 at 11:27
  • Maybe one of the CLR Master Jedi's like @EricLippert can shed light on the actual mechanism behind this – TheGeneral Oct 27 '18 at 11:44

2 Answers2

1

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

I doubt if it would be locking per-se, I guess the CLR just makes sure the IL is ordered an emitted in a way that its exclusive, though honestly i am not too sure.

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

Output

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

What you have here is not only extremely poorly written code, other threads have to wait around for these types of shenanigans to complete. So yes it can be slow if you choose it to be.


ECMA Specifications

15.12 Static constructors

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).

There is no mentioned of how it accomplishes exclusivity (as you would expect) as its just an implementation detail, however what we do know is it does.

And lastly, because looking through the specifications is barrel of fun and hilarity (Individual results may vary), there are more weird situations you can your self in, like making circular dependencies

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.

TheGeneral
  • 79,002
  • 9
  • 103
  • 141
  • Really interesting stuff, but I'm familiar with what the specification says. I want to know how an actual implementation achieves this. – Nick Oct 27 '18 at 11:03
  • How can the JIT know when to emit the IL? Does the JIT emit for every static member access a lock and check for when the static constructor is output, then rewrite those member accesses to be jumps to a specific memory location (which now exists since the static constructor is run?) – Nick Oct 27 '18 at 11:06
1

Let's first review the the different kinds of static constructors and the rules that specify when each must be executed. There are two kinds of static constructors: Precise and BeforeFieldInit. Static constructors that are explicitly defined are precise. If a class has initialized static fields without an explicitly defined static constructor, then the managed language compiler defines one that performs the initialization of these static fields. Precise constructors must execute just before accessing any field or calling any method of the type. BeforeFieldInit constructors must execute before the first static field access. Now I'll discuss when and how static constructors are called in CoreCLR and CLR.

When a method is called for the first time, a temporary entry point for that method gets called, which is mainly responsible for the JITing the IL code of the method. The temporary entry point (specifically, the prestub) checks the kind of the static constructor of the type of the method being called (irrespective of whether that method is instance of static). If it's Precise, then the temporary entry point ensures that the static constructor of that type has been executed.

The temporary entry point then invokes the JIT compiler to emit the native code of the method (since it's being called for the first time). The JIT compiler checks if the IL of the method includes accesses to static fields. For each accessed static field, if the static constructor of the type that defines that static field is BeforeFieldInit, then the compiler ensures that the static constructor of the type has been executed. Therefore, the native code of the method does not include any calls to the static constructor. Otherwise, if the static constructor of the type that defines that static field is Precise, the JIT compiler injects calls to the static constructor before every access to the static field in the native code of the method.

Static constructors are executed by calling CheckRunClassInitThrowing. This function basically checks whether the type has already been initialized, and if not, it calls DoRunClassInitThrowing, which is the one that actually calls the static constructor. Before calling a static constructor, the lock associated with that constructor needs to be acquired. There is one such lock for each type. However, these locks are created lazily. That is, only when the static constructor of a type gets called is a lock created for that type. Therefore, a list of locks needs to be maintained dynamically per appdomain and this list itself needs to be protected by a lock. So calling a static constructor involves two locks: an appdomain-specific lock and a type-specific lock. The following code shows how these two locks get acquired and released (some comments are mine).

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.
    }

    .
    .
    .

}

Static constructors of appdomain-neutral types and NGEN'ed types are handled differently. In addition, the CoreCLR implementation does not strictly adhere to the semantics of Precise constructors for performance reasons. For more information, refer to the comment at the top of corinfo.h.

Hadi Brais
  • 22,259
  • 3
  • 54
  • 95