|Async, Await and the UI Problem|
|Written by Mike James|
|Thursday, 25 February 2021|
Page 2 of 3
The answer is that we have to create a Task object that runs the worker code on a different thread. This how the UI thread can be released to do some other work and where the multithreading comes into the picture.
The alternative pattern is that the code in the task object simply waits for an external operation such as I/O to complete without explicitly starting a new thread. What matters in either case is that the UI thread is released.
There are a number of possible ways of creating a Task to do the job but the simplest is the Task static object which has a Run method that will take a delegate or a lambda expression and run it using thread from the threadpool.
To avoid a naming confusion the new Task static class is called TaskEx so the new DoWork method is:
You could use a delegate or an annoymous method to define the code to be run but a lambda expression is so much easier. This completes the conversion of the synchronous implementation to the asynchronous.
Now what do you expect to see if you run the program?
The answer is you see what you expected to see by a naive reading of the original program. That is, first you see "Click Started" appear, then there is a 5 second wait and "Click Finished" appears.
This just looks as if the DoWork method has been run in a blocking synchronous way - but no. The first program showed what happens if DoWork is run in a blocking synchronous way and this is different! If you look at the program more carefully you will also notice that the UI is responsive between the two messages appearing and we have solved the problem of the unresponsive UI. So what is happening?
At this point I have to say that the purpose of constructs such as async and await is to hide the details of what is going on behind the scenes - but you should still have a rough idea of what is happening.
What happens when the user clicks the Button is that the event is added to the Dispatcher queue and the UI thread eventually gets round to processing it.
This takes the UI thread to the button1_Click event handler and the UI thread updates textBlock1. This doesn't result in the change being rendered at this point, however.
Next the UI thread starts to execute the DoWork method but in this case as it's executed via an await within an async method it doesn't happen in the usual way. The UI thread creates the Task with the lambda expression that does all the work and returns to the event handler.
Of course all the while the UI thread is dealing with events and generally getting on with other stuff the instructions in the Task that was returned by DoWork are being obeyed and so 5 seconds later the Task completes.
At this point the need for the UI thread to return to the event handler is added to the Dispatcher's queue. Eventually the UI thread reaches this item in the Dispatcher's queue and starts executing the event handler from where it left it - i.e. just after the await. You might recognise this as a "continuation", a construct found in other languages such as Ruby, Haskell and Scheme.
At this point the second "Click Finished" message is executed and after this the event handler finishes in the usual way and once again the UI thread can return to the Dispatcher queue for more things to do.
The await causes the UI thread to return to the calling code, the Dispatcher in this case, and then it causes the UI thread to pickup where it left off when the Task completes - i.e. a continuation.
This may sound complicated and it is even more complicated if you enquire a little deeper into how it is all achieved. In particular the state of the event handler has to be saved when the await is obeyed and restored when the event handler resumes. But from the programmer's point of view it looks as if the await has run the task in DoWork while letting the UI thread get on with other things and then has called the UI thread back to finish the event handler once the Task is complete.
Notice that all of the code in the event handler and DoWork is run on the UI thread - only the lambda expression in DoWork is run on a different thread.
To make the point even more forcibly that await simplifies things compare the two versions and their flow of control:
private void button1_Click(object sender,
This looks as if it should display the first message, do something for 5 seconds and then display the second message - but of course it doesn't.
Adding the await command makes it do exactly that:
private async void button1_Click(
In the second case the apparent flow of control through the event handler coincides with what it appear to be.
|Last Updated ( Thursday, 25 February 2021 )|