|Managing Asynchronous Code - Callbacks, Promises & Async/Await|
|Written by Mike James|
|Wednesday, 12 August 2015|
Page 1 of 3
This is the second of two articles on asynchronous programming:
Asynchronous code is inevitable if we insist on the simplicity of a single threaded UI.
There are, however, other reasons for using asynchronous code such as efficiency. For example, Node.js is an asynchronous alternative to a multi-threaded web server like Apache. The argument is that async is appropriate as some sort of super light cooperative multi-tasking. This is another argument and for the rest of this article it will be assumed that the use of asynchronous code is necessary due to the more common demands of the single threaded UI.
Note that we already have our first "solution" to the asynchronous code problem - make the UI multi-threaded. In this case there is no need to write callbacks or anything similar because the thread dealing with the event can just enter a wait state until the long running task is complete. This is generally not thought to be a practical alternative because multi-threading a UI is difficult and in the past has lead to disaster.
For the time being the single threaded UI is going to be the norm and asynchronous code is here to stay - for a long while at least.
But this is getting to the end before we have completed the beginning.
What exactly is the problem we are trying to solve?
Many of the issues that arise with asynchronous programming have been discussed in the previous article - What Is Asynchronous Programming? You will hear lots of explanation of the problem of asynchronous code along the lines of "callback hell", and the "callback pyramid of doom". These are problems but they arise from taking particular approaches to asynchronous programming.
The first problem is that raw asynchronous programming distorts the intended flow of control.
In a synchronous program you might write
and you can expect A to load before B which loads before C.
As soon as you convert these to async operations you can't be sure what order things are done in unless you adopt the callback cascade:
where each accepts a callback as its parameter. The callback approach to async turns sequential default flow of control into nested function calls. But keep in mind that the callback approach is just one of many. Because it is so widely used there is a tendency to think that a callback is the only way to deal with asynchronous code.
Asynchronous code is essentially about co-operative multitasking with a single thread.
The idea is very simple.
Every time you write an event handler make it start a new thread and write all of the event handler code there. In this approach the UI thread only serves to signal to the event handler that it is needed and the event handler starts a new thread and returns at once.
This is a very standard approach to things like animation loops. The animation loop is often started by a user action which calls an event handler that spins up a timer which runs code on another thread every so often.
If you take this approach - every event handler is a new thread. You don't have an asynchronous problem but you do have a UI access problem. More generally you have parallel programming problems. This is why the UI isn't multithreaded by default.
Now if any event handler's new thread attempt to change the UI it will generally result in an exception. The reason is that most UI frameworks check to make sure that the thread trying to access a component is the UI thread. This is to ensure that multiple updates can't occur and that there are no race problems.
The general solution to this is to provide a function something like runOnUIThread() which defines a function that will be run on the UI thread within the code that is running on the new thread.
This works but you have just invented something very like a callback only from the non-UI thread to the UI thread.
However don't write this approach off too quickly.
For simple tasks the runOnUIThread code can be very small and simple - often just one instruction updating some part of the UI. The overall structure can seem simpler calling the UI from the non-UI thread. There is also the advantage that the non-UI thread determines when to call the UI update and so this integrates better with the logic of the task. That is the non-UI thread is closer to the simple synchronous code that you would have written if possible.
Even so the Callback is still the most common way of tackling the problem.
Asynchronous Flow Of Control
The effect that asynchronous code has on the structure of your program is easy to understand in terms of what code goes before and what code goes after:
The statements that are before the asynchronous operation provide the context for the call. Variables which are set in this before code may contain values that are important for what to do with the result of the asynchronous operation in the code to follow it. The code that follows it makes use of the result of the operation and the context established by the earlier code.
The Callback And The Closure
The callback is the most common approach to asynchronous code but it distorts the flow of control and potentially loses context.
For example when you convert the asynchronous call to a callback all of the code that follow it - the "after" code - becomes the callback:
You can now see that the flow of control has been distorted - what was one function is now two. Don't worry too much about it at the moment but this is a complete picture of what can happen because if the Callback contains an asynchronous call you repeat the procedure of moving the "after" code into a callback - distorting the flow of control again.
Not only that but in a simple programming environment the context is lost. As the "after" code is now a separate function it no longer has access to the variables contained in the "before" code. In short the callback can't perform an instruction like Text(1,i) because i isn't only out of scope it doesn't even exist.
This is the context bottle neck problem that callbacks introduce into asynchronous code. All of the data that was available to "after" now has to be sent to the callback as a set of parameters. Of course this doesn't work in practice because the LoadA function will often be a library function and you wont have control over its parameters. In other words LoadA will probably pass its result to the callback function(result) but it wont pass any variables from the context - function(result,i) say.
There are many complex and esoteric explanations of what closure is and why you might want it but it is this automatic provision of context to a callback function that seems the most convincing. There are lots of other uses of closure but it is this one that you would invent closure for.
Closures ensure that callbacks have their context.
Of course things can be more complicated. It could be that the asynchronous function is itself nested within a control structure.
This means that you not only have code before and after the asynchronous call there is code around it. This code that surrounds the call can be thought of as being part of the before and after code - but in reality it is quite a different type of problem. You have to implement something that converts the control structure into callbacks with a similar behaviour and this is not easy to solve in a completely general way. For example, one way to implement a general asynchronous loop is to convert it into a recursion - see What Is Asynchronous Programming?.
|Last Updated ( Friday, 31 March 2017 )|