I feel that the answer to this is due to me having an incorrect concept of how threads work, but here goes.
private void button1_Click(object sender, EventArgs e)
{
this.TestMethodAsync(); // No await, i.e. fire and forget
// ** Some code here to perform long running calculation (1) **
}
private async Task TestMethodAsync()
{
// Some synchronous stuff
await Task.Delay(1000);
// ** Some code here to perform long running calculation (2) **
}
First of all, I would not "fire and forget" an asynchronous method like this (I would use Task.Run) but I've come across code that does, and I'm trying to understand what the effect is.
In a WinForms application, which uses a WindowsFormsSynchronizationContext
, my understanding of async and await tells me that when I click button1, the method will start synchronously on the UI thread. It will call TestMethodAsync
and run synchronously until it reaches the await. It will then capture the context, start the Task.Delay
task, and yield control to the caller. Since we are not awaiting this call, button1_Click
will continue on the UI thread and start performing calculation (1).
At some point, Task.Delay(1000)
will complete. A continuation will then run the remainder of the TestMethodAsync
method using the captured context, which in this case means that the continuation will be run on the UI thread. This will now start performing calculation (2).
We now have two separate sections of code wanting to run on the same thread (the UI thread) at the same time. My investigations into this seem to suggest that the thread switches back and forth between the two sections of code in order to perform them both.
QUESTION:
I'm confused about exactly what is going on here. How is it possible to resume on a thread that is already running other code? What forces the thread to switch between the two sections of code that want to run? In general, what happens when you attempt to resume on a thread that is already running some other code?
(I suppose this isn't any different to how my click event runs on the UI thread in the first place, in as much as I know it runs on the UI thread, and I know the UI thread is also doing other stuff, but I've not really thought about it like this before.)
It needs to be specifically designed to support it. There needs to be some framework in place that allows the thread to take in work and to then execute that work at some later point in time.
This is how your UI thread works. It has a queue, and whenever you schedule work to be done in the UI thread you add an item to the end of the queue. The UI thread then takes the first item from the queue, executes it, and then when it's done, goes on to the next item, and so on, until you end your application.
Nothing, because it doesn't do that. It runs one, then when it finishes, it runs the other.
Either someone wrote some custom code to specifically do just that, in which case, it does whatever that code specifically told it to do, or else you can't.
This is the secret that you do not understand: I give you the Windows Message Loop
This is the actual "main" of your application; you just don't see it because it is hidden behind the scenes.
A simpler loop could not be imagined. It gets a message from the queue. If there are no more messages then the program must be done. If there was a message then it runs the standard message translations and dispatches the message, and then keeps on running.
It isn't. "Resuming on a thread that is running other code" is actually putting a message in the queue. That "other code" is being synchronously called by DispatchMessage. When it is done, it returns to the loop, the queue is polled, and the message indicates what code needs to be dispatched next. That then runs synchronously until it returns back to the loop.
Nothing. That doesn't happen.
The message that describes what continuation needs to be run is queued up.
Start thinking about it.
Click events are exactly the same. Your program is doing something; you click the mouse; the click hander does not interrupt the UI thread and start running new work on it. Rather, the message is queued up, and when your UI thread control returns to the message loop, the click is eventually processed; DispatchMessage causes Button1_OnClick to be invoked via some mechanism in Windows Forms. That's what WinForms is; a mechanism for translating Windows messages into calls to C# methods.
But you already knew that. You know that when an event-driven program does a long-running synchronous operation, that the UI freezes, but that click events are processed eventually. How did you think that happened? You must have understood at some level that they were being queued up for processing later, right?
Exercise: What does
DoEvents
do?Exercise: Given what you now know: what could possibly go wrong if you call
DoEvents
in a loop to unblock your UI?Exercise: How is
await
different fromDoEvents
in a GUI application?