Static constructor deadlocks

One important fact to know about static constructors is that they effectively execute under a lock. The CLR must ensure that each type is initialized exactly once, and so it uses locking to prevent multiple threads from executing the same static constructor. A caveat, however, is that executing the static constructor under a lock opens up the possibility of deadlocks.

This example demonstrates the deadlock:

    using System.Threading;
    class MyClass
    {
        static void Main() { /* Won’t run… the static constructor deadlocks */  }

        static MyClass()
        {
            Thread thread = new Thread(arg => { });
            thread.Start();
            thread.Join();
        }
    }

In the example above, the static constructor waits for the helper thread to complete. The thread executing the static constructor holds some CLR-internal lock. Then, if the CLR needs to acquire the CLR-internal lock in order to execute the helper thread – and that seems to be exactly what happens – a deadlock will occur.

This CLR behavior is defined in section 10.5.3.3 of the ECMA CLI specification:

“Type initialization alone shall not create a deadlock unless some code called from a type initializer (directly or indirectly) explicitly invokes blocking operations.”

So, any operation that blocks the current thread in a static constructor potentially risks a deadlock. For example, just as waiting on threads can deadlock a static constructor, waiting on a task can have the same effect. And the same goes for Parallel.For, Parallel.ForEach, Parallel.Invoke, PLINQ queries, etc.

This example uses Parallel.For and also deadlocks:

    using System.Threading.Tasks;
    class MyClass
    {
        static void Main() { /* Won’t run… the static constructor deadlocks */  }

        static int s_value = ComputeValue();

        private static int ComputeValue()
        {
            Action emptyAction = () => {};
            Parallel.Invoke(emptyAction, emptyAction);
            return 42;
        }
    }

As this example shows, Parallel.For called from a static constructor can also cause a deadlock. Also, notice that in the example above, the Parallel.For is actually called from a static initializer, not a static constructor. For all intents and purposes, static initializers execute as a part of the static constructor, and so they are also at risk of the same kind of deadlock.

So, to avoid the risk of deadlocks, avoid blocking the current thread in static constructors and initializers: don’t wait on tasks, threads, wait handles or events, don’t acquire locks, and don’t execute blocking parallel operations like parallel loops, Parallel.Invoke and PLINQ queries.