Search code examples
c#multithreadingdeadlockstatic-constructor

Do static constructor deadlocks in C# contradict the ECMA CLI standard?


Here is the section of the standard I'm confused by: http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-335.pdf#page=178&zoom=auto,87,610%22

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.

The following code deadlocks when I test it, which seems to contradict the standard:

public static class Foo {
    static Foo() {
        var otherThread = new Thread(() => { Thread.Sleep(1000); SomeFunction(); });
        otherThread.Start();
        otherThread.Join();
    }
    public static void SomeFunction() {
    }
}
class Program {
    static void Main() {
        Foo.SomeFunction();
    }
}

According to the standard, I expect the following to happen:

  1. The main thread takes the initialization lock on Foo.
  2. The main thread runs the static constructor of Foo.
  3. The main thread creates otherThread and starts it.
  4. otherThread begins waiting for one second, thus ensuring that point 5 happens before point 6.
  5. The main thread begins waiting for otherThread to complete.
  6. otherThread attempts to take the initialization lock on Foo, and fails because the main thread is holding the lock.
  7. otherThread gives up on executing the static constructor because the main thread is holding the initialization lock and waiting for otherThread.
  8. otherThread runs SomeFunction and finishes successfully.
  9. The main thread returns.

What's wrong here?


Solution

  • "any thread waiting for this thread to complete" refers to any thread waiting using the initialization locks for static threads, not a thread waiting using any possible synchronization mechanism. There's no way for the static initialization mechanism to know that some other thread is waiting using some entirely different mechanism on another thread.

    The quoted section is referring to the fact that the below example won't deadlock:

    public class A
    {
        static A()
        {
            Thread.Sleep(TimeSpan.FromSeconds(1));
            B.DoNothing();
        }
        public static void DoNothing() { }
    }
    public class B
    {
        static B()
        {
            Thread.Sleep(TimeSpan.FromSeconds(1));
            A.DoNothing();
        }
        public static void DoNothing() { }
    }
    private static void Main()
    {
        Task.Run(() => B.DoNothing());
        A.DoNothing();
    }
    

    This example doesn't deadlock because one thread is waiting for another thread to release the static initializer lock, so when that thread ends up asking for a static initializer lock that the original thread has, the quoted clause kicks in and it just skips the lock.