Static constructor deadlocks

Static constructor deadlocks

Rate This
  • Comments 7

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.

Leave a Comment
  • Please add 2 and 5 and type the answer here:
  • Post
  • Incredible, I couldn't imagine such a deadlock but it actually affected one of my projects just recently and your explanation came to rescue: stackoverflow.com/.../form-handlecreated-event-never-occurs

    Below code waits infinitely if in a static constructor but works normally if in any other function (static or not as long as it is not constructor):

       // Static constructor for one-time object initializations

       static InternalLogViewer()

       {

           viewer = new InternalLogViewer();

           formShown = new ManualResetEvent(false);

           viewer.HandleCreated += (sender, e) => formShown.Set();

           Task.Factory.StartNew(() => viewer.ShowDialog());

           formShown.WaitOne(); // ToDo: This needs a workaround as it waits for an eternity

       }

    Thanks for the fantastic clarification, which saved the day for me in the case above.

  • The second example doesn't deadlock on my computer

  • Meziantou: I tried the second example too, and it does seem to be bad. I replaced it with one that does seem to deadlock reliably.

  • Probably I'm missing something, but how come the CLR needs to acquire the internal lock again to run the helper thread??

  • Ay: It is not necessarily obvious why launching the helper thread acquires the lock. As a part of launching a helper thread, the CLR presumably either needs to initialize another type, or it acquires the internal lock for some other reason.

  • Igor: Thanks for answering! It's good to know that you guys are always ready to help :)

  • Acquiring the lock on  the helper thread is a first part. To obtain deadlock it is necessary that the CLR use the same lock object which used in static ctor running thread. Why they are equal at every times?

Page 1 of 1 (7 items)