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:
- Your
Task
is being returned fromTask.Delay
- The main thread starts synchronously waiting for this
Task
to complete, using a spin lock (inTask.SpinThenBlockingWait
) - The spin lock times out, and the main thread creates an event to wait on, which is set by a continuation on the Task
- The Task completes (you can see that it has, because its Status is RanToCompletion)
- The Task tries to complete the continuation which will release the event the main thread is waiting on (
Task.FinishContinuations
). This ends up callingTaskContinuation.RunCallback
(though I haven't traced that call path yet), which calls yourWindowsFormSynchronizationContext.Post
. - However,
Post
does nothing, and deadlock occurs.
To get that information, I did the following things:
- Try to call
new WindowsFormsSynchronizationContext.Post(d => ..., null)
, see that the delegate isn't called. - Construct my own
SynchronizationContext
and install it, see whenPost
gets called. - Break the debugger during the deadlock, look at
Threads
and look at theCall Stack
of the main thread. - 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 theWindowsFormsSynchronizationContext
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
}
}