Search code examples
c#.netdeadlockstatic-initializationstatic-constructor

How to safely work around BeforeFieldInit and static constructor cycles?


I'm concerned about the interaction between the following two behaviors:

http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-335.pdf#page=179

2.1. If the type is not yet initialized, try to take an initialization lock.

2.2.1. If not successful, see whether this thread or any thread waiting for this thread to complete already holds the lock.

2.2.2. If so, return since blocking would create a deadlock. This thread will now see an incompletely initialized state for the type, but no deadlock will arise.

http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-335.pdf#page=69

If marked BeforeFieldInit then the type's initializer method is executed at, or sometime before, first access to any static field defined for that type.

This code example demonstrates the possible interaction:

static class Foo {
  public static int A = 1;
}
static class Bar {
  public static int B = Foo.A;
}
static class Program {
  static void Main() {
    Console.WriteLine(Bar.B);
  }
}

When testing this in any sane environment, it will output 1. However, it seems that the spec allows it to output 0, by doing the following:

  1. Main starts executing.
  2. The type initializer for Foo starts executing (this is allowed to happen at this time because of the BeforeFieldInit rule).
  3. The type initializer for Bar starts executing (this is allowed to happen at this time because of the BeforeFieldInit rule).
  4. The initializer for Bar.B starts executing.
  5. Foo.A is requested.
  6. The type initializer for Foo is already running, and waiting on it would cause a deadlock. The deadlock rule allows us to look at Foo in an incompletely initialized state, where A has not yet been set to 1 and still has its default value of 0.
  7. Bar.B is set to 0.
  8. The type initializer for Bar is finished.
  9. Foo.A is set to 1.
  10. The type initializer for Foo is finished.
  11. Main outputs Bar.B, which is 0.

Is this really allowed? How am I supposed to write type initializers so I don't get bitten by it?


Solution

  • Is this really allowed?

    It sure looks like it is allowed by the spec.

    When testing this in any sane environment, it will output 1.

    That's correct. It's helpful to understand the reasoning behind the optimization. The purpose of the "relaxed semantics" is to move the check for "has the static constructor run?" from execution time of an access to the type to jit time of a method which accesses the type. That is, if we have:

    void M()
    {
        blah
        if blah blah
        ... Foo.A ...
        if blah blah blah
        ... Foo.A ...
        blah blah blah
    }
    

    Suppose it is jit time for M and Foo's cctor has not executed yet. To be strictly conforming, the jitter has to generate code on every access to Foo.A to check whether the Foo cctor has executed yet and execute it if it has not.

    But if we do the cctor call at jit time, the jitter knows that Foo is accessed inside M and so can call the cctor when M is jitted, and then skip generating each check inside M.

    The jitter is smart enough to do the right thing when executing the cctor at jit time; it does not execute the cctors in the "wrong" order as you describe, because the people who wrote the jitter were sane people who were just trying to make your code faster.

    How am I supposed to write type initializers so I don't get bitten by it?

    You should assume that the authors of the conforming implementation are sane.

    If for whatever reason you cannot assume that: you can put all the static field initializers you care about into a static constructor. The C# compiler will not allow BeforeFieldInit semantics on types with static constructors.