|What Is Asynchronous Programming?
|Written by Mike James
|Friday, 04 March 2022
Page 2 of 3
Now we come back to the problem of the single threading.
If any event handler takes a long time or worse never returns then the UI seems to freeze.
The reason is simply that if the event handler never returns the thread to the dispatcher and hence no other event handler ever gets to run. If the event handler holds the thread for any noticeable length of time then events are not processed and the user thinks that the UI is unresponsive for that time.
The cure for this problem is to keep the work done by any event handler to a minimum. In a sense the event handling system is a cooperative multitasker and it in this case it is the duty of every event handler to return control to the dispatcher as soon as possible. The fair sharing of the thread of execution is a matter of cooperation between all of the code that needs to run i.e. the event handlers.
So what do you do if an event handler has a substantial amount of work to do?
The textbook answer is that you simply use the event handler to set up another thread and get the new thread to do all the work. The event handler returns almost at once and the new thread continues to do the job.
This use of a new thread causes problems of synchronization that we will return to a little later. In short adding threads to a single threaded event system makes it much more complex.
Hence the non-textbook non-approved way of dealing with long running event handlers - doEvents.
Some languages have a special doEvents command which acts like a yield command - in that it transfers control to the calling routine, the dispatcher in this case, which then processes all of the pending events and when done transfers control back to the suspended event handler at the instruction following the doEvents. The state of the event handler is unchanged at it continues on as if nothing had happened. In other words the doEvents command does what it says it does - it pauses the event handler and allows the dispatcher to "do" any pending events so keeping the UI responsive.
Languages that don't have a doEvents or similar can often implement the same thing if they provide access to the dispatcher from running event handlers. Basically the event handler puts an event record at the end of the queue that restarts it and then returns control to the dispatcher.
This seems the ideal solution.
A long running event handler cooperatively yields the thread of execution so that other events can be processed and the UI kept active. When the events have been processed then the long running event handler is resumed.
It is indeed a good solution but it has a flaw that is so serious that many give the advice that it is to be avoided at all cost and its bad practice.
The flaw is simply that the event handler that yields control is now no longer atomic.
It is interrupted at some point in its code and other event handlers could change things that it is using. Worse it could even be called again as the result of an event in the queue. Most event handlers are not reenterable and things could get very complicated.
However a programmer, aware of these difficulties can make doEvents work. You have to take account of the fact that shared resources could change after a doEvents and you have to reject any attempt to start the event handler again while is is still active.
Both of these are usually easy to achieve and arguably much easier than starting a worker thread to get the same job done.
However for beginners the rule "don't use doEvents" is a safe one.
The idea of having an event handler return almost at once is the ideal in a single threaded event driven system.
This is often refered to as a non-blocking call as opposed to a blocking call which stops the rest of the system moving on because it takes so long. In most cases a blocking call can be converted to a non-blocking call by the simple expedient of returning before its job is done.
We have already met one big example of converting a blocking to a non-blocking call when the idea of using a new thread to get an event handlers task completed.
The idea is that if you have a event handler
then you convert it into:
This looks very simple but it raises a whole new problem. Usually the way that the applciation behaves depends on the completion of the event handler's task.
For example if the event handler was say loading an image over the internet then perhaps the next action that the user takes depends on its completion. The point is that now the application needs to know when the getWorkDone function has completed.
In other words using a separate thread to complete the work of the event handler has produced a synchronization problem that disrupts the logical flow of the program.
It also introduces other problems associated with the thread shareing resources and accessing the UI but these are not really central to the problems of asynchronous code.
Introducing a thread makes things more complex but we have little choice.
The idea of converting a blocking function into a non-blocking one extends to functions which are not themselves event handlers. For example consider the getWorkDone function which takes a long time to complete.
You could make it non-blocking by introducing a new thread.
In the case of many system or framework functions you don't even have to imagine that they create a new thread you can regard them as simply returning at once even thought their job hasn't been completed.
Using the non-blocking form of the getWorkDone function the event handler can be written more simply as:
Notice that now the writer of the event handler doesn't have to worry about creating a new thread or how to run getWorkDone to minimize the slowdown of the UI.
However this solution, neat though it is, introduces another problem. Usually the event handler would want to do something with the result of getWorkDone. In general when ever a blocking function is converted into a non-blocking function we have the problem of what to do next. Indeed we have a problem of exactly when "next" is.
The problem is when getWorkDone completes some seconds after onEvent has returned there is no calling function for it to return to. The calling function terminated a while ago by returning so that the UI thread can be ready to process another event.
|Last Updated ( Friday, 04 March 2022 )