Reputation: 2656
As a learning exercise, I'm trying to reproduce an async/await deadlock that occurs in a normal windows form, but using a console app. I was hoping the code below would cause this to happen, and indeed it does. But the deadlock also happens unexpectedly when using await.
using System;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
static class Program
{
static async Task Main(string[] args)
{
// no deadlocks when this line is commented out (as expected)
SynchronizationContext.SetSynchronizationContext(new WindowsFormsSynchronizationContext());
Console.WriteLine("before");
//DoAsync().Wait(); // deadlock expected...and occurs
await DoAsync(); // deadlock not expected...but also occurs???
Console.WriteLine("after");
}
static async Task DoAsync()
{
await Task.Delay(100);
}
}
I'm mostly curious if anyone knows why this is happening?
Upvotes: 6
Views: 1052
Reputation: 43573
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.
Upvotes: 3
Reputation: 42235
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:
Task
is being returned from Task.Delay
Task
to complete, using a spin lock (in Task.SpinThenBlockingWait
)Task.FinishContinuations
). This ends up calling TaskContinuation.RunCallback
(though I haven't traced that call path yet), which calls your WindowsFormSynchronizationContext.Post
.Post
does nothing, and deadlock occurs.To get that information, I did the following things:
new WindowsFormsSynchronizationContext.Post(d => ..., null)
, see that the delegate isn't called.SynchronizationContext
and install it, see when Post
gets called.Threads
and look at the Call Stack
of the main thread.Upvotes: 3
Reputation: 5798
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
}
}
Upvotes: 2