Why await? Why not multithread the UI? |
Written by Mike James |
Monday, 24 October 2011 |
We explore some of the questions surrounding asynchronous programming and reveal that the need for new constructs, such as await in C#, isn't due to deep considerations, but results from the fact that the UI is single threaded. There is currently a lot of ground-breaking work on the subject of asynchronous programming and how to make it easier for programmers to make use of the asynchronous nature of today's environments. The idea is to restore the correspondence between the programs text and the order of execution by using some interesting compiler rewriting techniques. While this is a step forward the real situation is more complicated. In fact this is a mess of our own creation and with a little more though no solution would have been necessary. When the modern GUI was invent some means of attracting the programs attention to what was going on was needed. We could have opted for a polling loop. For example a rule of the UI could have been that every program took the form: while(true){ If this sounds crazy you have been working with events too long. Events in most GUIs are just a way of covering up the polling loop. EventsInstead of polling the designers opted for an event based system of asynchronous procedure calls. Instead of a polling loop any control that needs attention, a button that has been clicked, places a record in a queue maintained by a dispatcher object running on the UI thread. The jargon isn't universal, but the way things work is more or less so. The event record can get into the dispatcher queue by way of another thread, but once it is in the queue it is then only processed by the UI thread and this is where the problems start. The UI is really is written in the form of a polling loop. Rather than you writing the polling loop, it is part of the internals of the framework. The UI simply scans the dispatcher object's queue for new event records. As soon as it finds one, it calls the event handlers assigned to the event, if any. Notice that it is the UI thread that is used to run the event handlers and while it is doing this the dispatcher object is not doing anything. This doesn't stop other threads putting event records into its queue, however. Freezing the threadAs long at the UI thread is being used to run an event handler it isn't available to poll the dispatchers queue and so no events get processed. This is the reason that a long running event handler will cause the UI to freeze. This is true of Windows forms, WPF, Silverlight, HTML, Swing, etc. In nearly all cases worth mentioning the use of a single threaded dispatcher results in the UI freezing if any event handler performs a task that takes a lot of time. So how do you let an event handler do a lot of time-consuming work? The standard way is to spin the work off into a worker thread that then releases the UI thread to process the dispatcher queue and so keep the UI responsive. However this takes the average programmer into the realm of multithreaded programming something that even the above average programmer makes mistakes with. The traditional alternative to multithreading is to allow the dispatcher some time to process the queue every now and again - for example the much derided DoEvents command in Win Forms. A DoEvents command simply calls the dispatcher to process its queue with the UI thread returning to the instruction after the DoEvents when the queue is empty. By putting DoEvents commands at regular intervals within a long-running event handler, the UI can be kept responsive. So what is the problem with DoEvent style commands? The usual objection is that the current event handler might be reactivated by accident and a nested sequence of DoEvents could bring the system to a halt. In practice DoEvents is also a difficult one to deal with, because there is no guarantee how long the UI thread will be occupied servicing other event code. There is also the small matter that other event handlers could make changes to variables. In other words the event handler that yielded the UI thread might discover that things have changed when it gets control back. However, this is not as bad as a true multithread situation as there is only one thread doing the work and race hazards can't occur. In this situation all event handlers have to be written with the idea that all of the in-scope variables are volatile and can change their values. Notice that this is a problem that exists no matter what mechanism you use to allow the UI thread to run the dispatcher while an event handler is active. So what is the current solution all about? AwaitIt isn't only event handlers that have a lot of work to do that are the problem. Sometimes an event handler that doesn't have much work to do stops the system in its tracks because it makes use of a service which is slow. It downloads a file from the internet, say. Usually this is done by calling another method and in this case the problem of blocking the UI thread is solved not by a "DoEvents" mechanism but by using a callback. A callback is just a function that is passed to the method doing the work that it promises to call when the job is complete. Notice that the callback mechanism only works if the method using the callback terminates very soon after setting it. Only then can the UI thread be released and the dispatcher serviced. Notice that there is a subtlety here in that the callback has to be run on the UI thread and this either has to involve the dispatcher or some other similar mechanism to redirect the UI thread's attention to the callback function. Essentially the callback function represents a continuation of the original method after the time consuming process is complete. Unfortunately the continuation is a far from perfect one. Usually little, if any, of the state of the method that issued the callback is preserved. That is, the callback starts off as a new function with no knowledge of what the original method was doing, other than what is conveyed in its parameters. Providing some of the state to a callback function is what closures are all about - a callback function can have access to the variables that were local when it was defined, even if those variable are now out of scope and supposedly destroyed. The problem of loss of context when a callback is used to continue a method can be illustrated by a very simple example. Imagine you want to download a single file from the internet. You might write: downloadfile(mycallback); and expect the mycallback function to store the downloaded file when the download was complete. Easy - and because the method ends after the call to downloadfile, the UI thread is released only to continue the method's work later when mycallback is invoked. Now consider how you might download 25 files in the same way. The obvious thing to do is use a for loop but you can get the call back to continue a for loop automatically - the context is lost. So what can we do? The current solution offered by C# 4.5 is interesting. It simply takes the idea of the continuation seriously. You can now write something like: for(int i=0;i<25;i++){ Now we don't need a callback because what happens is that the UI thread is released at the await until the download is complete, when the method continues as if nothing had happened. Notice that, from the point of view of the innocent programmer, nothing much exciting has happened. The program has worked by executing commands in the order in which they are written and the call to downloadfile() is nothing special, apart from the need to put await infront of it. Of course, we know that at the await the UI thread was released and it got on with other tasks until the downloadfile call completed and the method was resumed with the mycallback function. (There is no reason to use a function or to call it mycallback - this is just for continuity with the previous example.) Notice that the await approach doesn't really do much more than the DoEvent approach, apart from avoiding the problem of re-entering the method that used the DoEvent call. Multithread the UI?Overall this seems like a lot of machinery to solve a simple problem - that the UI is single threaded. Surely in this more advanced age it is time to implement a UI that isn't single threaded. Currently the dispatcher approach to implementing the UI is simply a gloss over the polling approach that seems so crude. It implements a sort of co-operative multitasking when we are quite capable of implementing a true multitasking architecture. Why does the dispatcher have to run on the same thread as the event handlers? This may be simple but it causes all of the problems we have seen earlier and forces the compiler designers to introduce keywords like await and their corresponding semantics, simply because the UI is single threaded and not because of some deep fundamental need of asynchronous programming. If the dispatcher ran on its own thread and invoked event handlers using additional threads then it wouldn't matter at all if an event handler held onto its thread for a long time - it wouldn't block anything. Of course, this is true multitasking and as such has some inherent dangers, but the dispatcher could apply some very simple rules to make sure nothing really terrible happened. It could ensure that only one thread was allocated at a time to control, so avoiding the problem of re-entrancy, i.e. an event cannot occur while its event handler is active. To make things simpler we could share a single thread between the UI components unless they were marked as concurrent and so on. The point is that we could invent some new mechanisms to protect the UI from accidental multithreading while making it easy to allow it where it was beneficial. I'm not suggesting that making the UI multithreaded is an easy option, but I am suggesting it is the worthwhile option and tinkering with asynchronous constructs such as await is really not getting to the heart of the problem. If you build a real user interface using electronics then being able to press more than one button at a time is the way the world works, and it is the way the programmatic UI should work as well. The strange fact is that we are adding core language constructs to deal with a problem of framework design. Make the UI multithreaded and the need for await goes away. |
Last Updated ( Monday, 24 October 2011 ) |