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?
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.
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.
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.
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();
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With