async/await deadlock when using WindowsFormsSynchronizationContext in a console app

WindowsFormsSynchronizationContext will post any delegates its given to a WinForms message loop, which is serviced by a UI thread. However you never set one of those up and there is no UI thread, so anything you post will simply disappear.

So your await is capturing a SynchronizationContext which will never run any completions.

What's happening is:

  1. Your Task is being returned from Task.Delay
  2. The main thread starts synchronously waiting for this Task to complete, using a spin lock (in Task.SpinThenBlockingWait)
  3. The spin lock times out, and the main thread creates an event to wait on, which is set by a continuation on the Task
  4. The Task completes (you can see that it has, because its Status is RanToCompletion)
  5. The Task tries to complete the continuation which will release the event the main thread is waiting on (Task.FinishContinuations). This ends up calling TaskContinuation.RunCallback (though I haven't traced that call path yet), which calls your WindowsFormSynchronizationContext.Post.
  6. However, Post does nothing, and deadlock occurs.

To get that information, I did the following things:

  1. Try to call new WindowsFormsSynchronizationContext.Post(d => ..., null), see that the delegate isn't called.
  2. Construct my own SynchronizationContext and install it, see when Post gets called.
  3. Break the debugger during the deadlock, look at Threads and look at the Call Stack of the main thread.
  4. Capture the task being awaited in a variable, look at it in a watch window, right-click -> Make Object ID, then put that Object ID in the watch window. Let it deadlock, break, and inspect the task in the watch window from its Object ID.

This happens because the WindowsFormsSynchronizationContext depends on the existence of a standard Windows message loop. A console application does not start such a loop, so the messages posted to the WindowsFormsSynchronizationContext are not processed, the task continuations are not invoked, and so the program hangs on the first await. You can confirm the non-existence of a message loop by querying the boolean property Application.MessageLoop.

Gets a value indicating whether a message loop exists on this thread.

To make the WindowsFormsSynchronizationContext functional you must start a message loop. It can be done like this:

static void Main(string[] args)
{
    EventHandler idleHandler = null;
    idleHandler = async (sender, e) =>
    {
        Application.Idle -= idleHandler;
        await MyMain(args);
        Application.ExitThread();
    };
    Application.Idle += idleHandler;
    Application.Run();
}

The MyMain method is your current Main method, renamed.


Update: Actually the Application.Run method installs automatically a WindowsFormsSynchronizationContext in the current thread, so you don't have to do it explicitly. If you want you can prevent this automatic installation, be configuring the property WindowsFormsSynchronizationContext.AutoInstall before calling Application.Run.

The AutoInstall property determines whether the WindowsFormsSynchronizationContext is installed when a control is created, or when a message loop is started.


I believe it's because async Task Main is nothing more than syntax sugar. In reality it looks like:

static void Main(string[] args) => MainAsync(args).GetAwaiter().GetResult();

I.e. it's still blocking. Continuation of DoAsync is trying to execute on original thread because synchronization context isn't null. But the thread is stuck because it's waiting when task is completed. You can fix it like this:

static class Program
{
    static async Task Main(string[] args)
    {
        SynchronizationContext.SetSynchronizationContext(new WindowsFormsSynchronizationContext());
        Console.WriteLine("before");
        await DoAsync().ConfigureAwait(false); //skip sync.context
        Console.WriteLine("after");
    }
    static async Task DoAsync()
    {
        await Task.Delay(100).ConfigureAwait(false); //skip sync.context
    }
}

Tags:

C#

Async Await