10

I thought I understood the async-wait pattern and the Task.Run operation.
But I am wondering why in the following code example the await does not sync back to the UI thread after returning from the finished task.

public async Task InitializeAsync()
{
    Console.WriteLine($"Thread: {Thread.CurrentThread.ManagedThreadId}"); // "Thread: 1"
    double value = await Task.Run(() =>
    {
        Console.WriteLine($"Thread: {Thread.CurrentThread.ManagedThreadId}"); // Thread: 6

        // Do some CPU expensive stuff
        double x = 42;
        for (int i = 0; i < 100000000; i++)
        {
            x += i - Math.PI;
        }
        return x;
    }).ConfigureAwait(true);
    Console.WriteLine($"Result: {value}");
    Console.WriteLine($"Thread: {Thread.CurrentThread.ManagedThreadId}"); // Thread: 6  - WHY??
}

This code runs within a .NET Framework WPF application on a Windows 10 system with attached Visual Studio 2019 Debugger.
I am calling this code from the constructor of my App class.

public App()
{
    this.InitializeAsync().ConfigureAwait(true);
}

Maybe it is not the the best way, but I am not sure if this is the reason for the weird behaviour.

The code starts with the UI thread and should do some Task. With the await operation and ConfigureAwait(true) after the Task finishes it should continue on the Main Thread (1). But it does not.

Why?

Boann
  • 48,794
  • 16
  • 117
  • 146
rittergig
  • 715
  • 1
  • 5
  • 16

1 Answers1

13

It's a tricky thing.

You are calling await on UI thread, it's true. But! You are doing it inside App's constructor.

Remember that the implicitly generated startup code looks like this:

public static void Main()
{
    var app = new YourNamespace.App();
    app.InitializeComponent();
    app.Run();
}

The event loop, which is used for returning back to the main thread, is started only as a part of Run execution. So during the App constructor run, there is no event loop. Yet.

As a consequence, the SynchronizationContext, which is technically responsible for return of the flow to the main thread after await, is null at the App's constructor.

(SynchronizationContext is captured by await before waiting, so it doesn't matter that after finishing the Task there is already a valid SynchronizationContext: the captured value is null, so await continues execution on a thread pool thread.)

So the problem is not that you are running the code in a constructor, the problem is that you are running it in the App's constructor, at which point the application is not yet fully set up for execution. The same code in MainWindow's constructor would behave well.

Let's make some experiment:

public App()
{
    Console.WriteLine($"sc = {SynchronizationContext.Current?.ToString() ?? "null"}");
}

protected override void OnStartup(StartupEventArgs e)
{
    Console.WriteLine($"sc = {SynchronizationContext.Current?.ToString() ?? "null"}");
    base.OnStartup(e);
}

The first output gives

sc = null

the second

sc = System.Windows.Threading.DispatcherSynchronizationContext

So you can see that already in OnStartup there is a synchronization context. So if you move InitializeAsync() into OnStartup, it will behave as you'd expect it.

Vlad
  • 35,022
  • 6
  • 77
  • 199