Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why doesn't await Task.Run() sync back to UI Thread / origin context?

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?

like image 291
rittergig Avatar asked Dec 07 '19 11:12

rittergig


People also ask

Does Task run use a new thread?

NET code does not mean there are separate new threads involved. Generally when using Task. Run() or similar constructs, a task runs on a separate thread (mostly a managed thread-pool one), managed by the . NET CLR.

Does Task run Use thread pool?

Inside DoComplexCalculusAsync(), Task. Run uses another new thread from thread pool to do the heavy calculations in the background. Thus the thread pool has to deal with unexpectedly loosing one thread from the pool.

What does await Task run do?

As you probably recall, await captures information about the current thread when used with Task. Run . It does that so execution can continue on the original thread when it is done processing on the other thread.

How to run a Task in C#?

To start a task in C#, follow any of the below given ways. Use a delegate to start a task. Task t = new Task(delegate { PrintMessage(); }); t. Start();


1 Answers

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.

like image 71
Vlad Avatar answered Sep 28 '22 19:09

Vlad