Besides the issues previously described, there is even more pain/frustration to cope with, which I'd like to discuss in this blog post. The main motivation to write this down is because I'm a control freak that wants to know what happens, it took me some effort discovering these details, and I have found myself repeating the same stuff to others.
Executing tasks concurrently
In addition to organizing data in a program, the ability to execute tasks concurrently is also important for many types of applications, such as:
- Applications with graphical user interfaces (GUIs). For example, if a user clicks on a button executing a task, the GUI must remain responsive while the task is being processed.
- Server applications. A server application has to serve multiple connected clients simultaneously, while each task remains responsive. For example, if a task is being processed of some client, it should not be necessary for a new client to wait until the requests of the others are done.
- Applications communicating with hardware peripherals. Many peripherals, such as (hard/DVD/Blu-Ray) disk drives or printers are significantly slower than the CPU of the computer. For example, it's quite inconvenient that execution of a program temporarily blocks the entire system when a file is being read from the hard drive. Instead, it's (obviously) better to keep the programming running (and respond on user events) while a file is being read, and to notify the user when the task is done.
To allow tasks to be executed concurrently, many operating systems/execution environments have facilities, such as interrupts, processes and/or threads allowing a developer to do that in a (slightly) convenient way. The execution environment ensures that these processes or threads are executed simultaneously (well not really, in fact it divides CPU execution time over processes and threads, but in order to keep this blog post short, I won't go into detail on that).
Executing tasks concurrently in JavaScript
Although most operating systems, execution environments, and programming languages have facilities for threads and/or processes, JavaScript -- a programming language originally used inside a web browser with increasing popularity outside the browser -- does not have such facilities. In JavaScript, there is only one single execution thread.
Consider the following example HTML page:
<!DOCTYPE html> <html> <head><title>Stretching button</title></head> <body> <h1>Stretching button!</h1> <button id="button" onclick="stretchButton();">Stretch</button> <script type="text/javascript"> enabled = false; function updateButtonSizes() { var grow = true; var width = 100; var height = 20; var button = document.getElementById('button'); while(enabled) { /* Update the button sizes */ button.style.width = width + "px"; button.style.height = height + "px"; /* Adjust the button sizes */ if(grow) { width++; height++; } else { width--; height--; } /* If the limits are reached, stretch in * the opposite direction */ if(width >= 150) grow = false; else if(width <= 100) grow = true; } } function stretchButton() { if(enabled) { enabled = false; } else { enabled = true; updateButtonSizes(); } } </script> </body> </html>
The purpose of the HTML code shown above is to display a button that animates by increasing and decreasing its width and height once the user has clicked on it. The animation keeps repeating itself and stops if the user clicks on the same button again.
By just looking at the the code, it may give you the impression that it should work fine. However, once we have clicked on the button, the browser blocks completely and prevents us clicking on the button again. Furthermore, because we cannot click on the button again, we are also incapable of stopping the animation sequence. As a consequence, we have run into an infinite loop.
The reason that we run into trouble, is because if JavaScript code is being executed -- that runs in a single execution thread -- the browser cannot do anything else, such as responding to user events or updating the screen. After a minute or so, the browser will notice that some script is blocking it and asks you if it should terminate the script for you.
So you may probably wonder how to deal with this blocking issue? To allow the browser to respond to user events and update the screen, we must stop the execution of the program (by reaching the end of its control flow). Then at a later stage, we must resume execution by generating an event. Besides generating events from page elements, such as buttons as can be seen in the code fragment above, we can also generate timeout events:
setTimeout(function() { document.write("Hello world!<br>"); }, 1000);
In the snippet above, the setTimeout() function invocation tells the browser to execute the function displaying "Hello World" after 1000 milliseconds. Like events generated by page elements, timed events can also only be processed if nothing is being executed at that moment. So the program must reach the end of its execution flow first before the timeout event can be processed.
An adapted version of the earlier example with the button that correctly handles events and screen updates, may look as follows:
<!DOCTYPE html> <html> <head> <title>Stretching button</title> </head> <body> <h1>Stretching button!</h1> <button id="button" onclick="stretchButton();">Stretch</button> <script type="text/javascript"> enabled = false; button = document.getElementById('button'); function updateButtonSizes() { /* Update the button sizes */ button.style.width = width + "px"; button.style.height = height + "px"; /* Adjust the button sizes */ if(grow) { width++; height++; } else { width--; height--; } /* If the limits are reached then stretch in * the opposite direction */ if(width >= 150) grow = false; else if(width <= 100) grow = true; /* Keep repeating this until the user disables it */ if(enabled) setTimeout(updateButtonSizes, 0); } function stretchButton() { grow = true; width = 100; height = 20; if(enabled) { enabled = false; } else { enabled = true; updateButtonSizes(); } } </script> </body> </html>
The above example code is quite similar to the previous one, with the following differences:
- The updateButtonSizes() function does not contain a while loop. Instead, it implements a single iteration step and calls setTimeout() to generate a timeout event that calls itself.
- After setting the timeout event, the program reaches the end of the execution flow.
- The browser updates the screen, handles user input and resumes execution by processing the timeout event that invokes updateButtonSizes() again.
- I also made several variables global so that their values are remembered after each execution of updateButtonSizes()
To prove that the above example works, I have included an embedded version in this blog post:
Stretching button!
Cooperative multi-tasking
In short, to execute tasks in a browser while keeping the user interface responsive, we must do the following:
- Generate timeout events when it's desired to give other tasks the opportunity to do something.
- Reach the end of the control flow so that events can be processed by the execution environment. The generated timeout event calling a function ensures that execution is resumed at a later stage.
This approach looks very similar to cooperative multi-tasking using by classic Mac OS 9 and earlier and Windows operating systems prior to 3.x.
The fact that a function needs to be called every time you want to allow the browser/runtime environment to process events, reminds of the way I was developing programs many years ago on the AmigaOS. I still remember quite vividly that when using AMOS BASIC, if I wanted Workbench applications running in the background to do something while my program was running, I had to add the 'Multi Wait' instruction to the main loop of the program:
While busy ' Perform some tasks Multi Wait Wend
By using the 'Multi Wait' instruction, the operating system gets notified, interrupts the program's execution and the scheduler decides which other process is allowed to use the CPU. Then at some point, the OS scheduler decides that my program is allowed to continue and the program's execution continues as if nothing happended.
However, compared the JavaScript approach, the AmigaOS approach is a bit more powerful since it also takes care of interrupting and resuming a program at the right location within the right context.
Cooperative multitasking works as long as tasks properly cooperate, putting great responsibility by developers of a program. It's not uncommon however, that people make mistakes and the system as a whole blocks forever.
Asynchronous programming
Apart from the browser, many other environments embedding a JavaScript runtime require developers to use cooperative multitasking techniques.
Node.js, a server-side software system designed for writing scalable Internet applications, uses almost the same approach as the browser just described earlier. One minor difference is that instead of using setTimeout(), a more efficient alternative is used:
process.nextTick(function() { process.stdout.write("Hello world!\n"); });
The process.nextTick() function executes the given callback function once the end of the execution flow of the program has been reached. Because it does not use the timer, it's more efficient.
To save users the burden of calling process.nextTick() and ending the execution flow each time something may block the system, most library functions in Node.js are asynchronous by default (with some having a synchronous alternative). Asynchronous functions have the following properties:
- They return (almost) immediately.
- A callback function (provided as function parameter) gets invoked when the task is done, which can be used to retrieve its result or status.
- They automatically generate tick events and reach the end of their control flow to prevent the system being blocked.
For example, to read a file and store its contents in a string without blocking the system, one could do:
var fs = require('fs'); fs.readFile("/etc/hosts", function(err, data) { if(err) process.stderr.write("Cannot read /etc/hosts\n"); else process.stdout.write("/etc/hosts contains:\n" + data); }); process.stderr.write("Hello!\n");
The above example opens /etc/hosts asynchronously. The fs.readFile() function returns immediately, then the string "Hello" is printed and the program reaches the end of its control flow. Once the file has been read, the callback function is invoked displaying either an error message (if the file cannot be opened) or the contents of the file.
Although asynchronous functions prevent the system being blocked and allow multitasking, one of its big drawbacks is that the callback mechanism makes it much more difficult to structure a program properly. For example, if I want to create a folder called "out", then a subfolder called "test" in the previous folder, inside the "test" subfolder a file called "hello.txt" containing "Hello world!", and finally verify whether the written text file contains "Hello world!", we may end up writing:
var fs = require('fs'); var path = require('path'); fs.mkdir("out", 0755, function(err) { if(err) throw err; fs.mkdir(path.join("out, "test"), 0755, function(err) { if (err) throw err; var filename = path.join("out", "test", "hello.txt"); fs.writeFile(filename, "Hello world!", function(err) { if(err) throw err; fs.readFile(filename, function(err, data) { if(err) throw err; if(data == "Hello world!") process.stderr.write("File is correct!\n"); else process.stderr.write("File is incorrect!\n"); }); }); }); });
For each step in the above sequential program structure, we end up one indentation level deeper. For the above example it's probably not so bad, but what if we have to execute 10 asynchronous functions sequentially?
Moreover, it is also non-trivial to modify the above piece of code. What, for example, if we want to write another text file, before the function that creates the test folder? It requires us to weave another function callback in the middle of the program and to refactor the entire indentation structure.
Fortunately, there are also solutions to cope with that. For example, the async library contains a collection of functions supporting control flow patterns and functions supporting data collections that can be used to deal with complexities of asynchronous functions.
The waterfall pattern, for example, can be used to execute a collection of asynchronous function sequentially and stops if any of the callbacks returns an error. This can be used to flatten the structure of our previous code example and also makes it better maintainable:
var fs = require('fs'); var path = require('path'); filename = path.join("out", "test", "hello.txt"); async.waterfall([ function(callback) { fs.mkdir("out", 0755, callback); }, function(callback) { fs.mkdir(path.join("out, "test"), 0755, callback); }, function(callback) { fs.writeFile(filename, "Hello world!", callback); }, function(callback) { fs.readFile(filename, callback); }, function(data, callback) { if(data == "Hello world!") process.stderr.write("File is correct!\n"); else process.stderr.write("File is incorrect!\n"); } ], function(err, result) { if(err) throw err; });
Besides the waterfall pattern, the async library supports many other control flow and collection patterns, such as executing functions in parallel or asynchronous loops over items in an array.
Mutable state
There is another important aspect that we have to keep in mind while executing stuff concurrently. Shared mutable state may influence the result of an execution of a task. For example, in our last example using async.waterfall(), I have declared the filename variable as a global variable, because I (naively) thought that it was a good way to share its value among the callback functions.
However, since it's possible to run multiple tasks concurrently, another task may change this global variable to something else. As a consequence, when we have reached the callback using it, it may has been changed to a different value and thus our expected result may differ. Moreover, it's also extremely hard to debug these kind of problems.
To protect ourselves from these kind of problems, it's better to scope a variable, which can be done with the var keyword. However, although most programming languages, such as Java (which influenced JavaScript) use block-level scoping, in JavaScript variables are function-level scoped.
For example, the following Java example shows the effect of block-level scoping:
public class Test { public static void main(String[] args) { int i = 1; int j = 0; if(j == 0) { int i = 2; } return i; // 1; } }
In the example above we have two i variables. The first one is declared inside the main() body, the second inside the if-statement block. Because the variables are scoped within the nearest block boundary, the value of i inside the if-block is discarded after it terminates.
However, a direct port of the above code to JavaScript yields a different result, because i is scoped to the nearest function boundary:
function test() { var i = 1; var j = 0; if(j == 0) { var i = 2; } return i; // 2; }
So in our JavaScript code fragment, the second var i; declaration inside the if-block is also bound to the scope of the test() function, therefore yielding a different result. Fortunately, block level scoping can be easily simulated by declaring a function without parameters and calling it without parameters (function() { var x; ... } ();):
function test() { var i = 1; var j = 0; if(j == 0) { function() { var i = 2; /* i is scoped within the surrounding function */ }(); } return i; // 1; }
Similarly this trick also allows us to scope the filename variable shown in our earlier example with async.waterfall():
function() { var filename; /* filename is scoped within the surrounding function */ async.waterfall(...); }();
In the above example, the filename variable is not global anymore. The callback functions inside async.waterfall() will not refer to the global variable named filename any longer, but to the scoped variable inside the function block. As a result, other tasks running concurrently adapting global variables cannot influence this task any longer.
Conclusion
In this blog post, I have described some of the pains I have experienced with multitasking inside a JavaScript environment. From this, I have learned the following lessons:
- If a script is being executed, then nothing else can be done, such as screen updates, processing user events, handling client connections etc.
- To allow multitasking (and prevent the system being blocked entirely) we have to cooperate by generating timeout or tick events and ending the control flow of the program.
- Asynchronous functions use callbacks to retrieve their results and/or statuses making it extremely hard to structure your program properly. In other words, it easily becomes a mess. To cope with this, you need to think about patterns to combine them. Libraries, such as async, may help you with this.
- Shared state is evil, error prone and makes a program hard/impossible to debug. Therefore, only use global variables that are immutable or refer to singleton instances (such as CommonJS modules). Otherwise, scope them.
- JavaScript does not support block level scoping, but it can be simulated by encapsulating a block inside a function that gets called without parameters. Fortunately, the ECMAScript 6 standard defines the let keyword supporting block-level scoping natively, but it's not supported in most implementations, such as in Node.js.
Although I'm a little happy about the fact that I know how to handle these problems, I'm very sad at the same time because of the following reasons:
- JavaScript was originally advertised as "a lightweight and simpler variant of Java". The fact that every JavaScript developer must cope with all these concurrency issues, which Java mostly solves with a threading API, definitely makes JavaScript NOT simpler than Java in this respect IMHO.
- Cooperative multitasking is a technique that was very common/useful in the early 1980s (and probably invented years before it), but has been replaced by preemptive multitasking in modern operating systems. One of the reasons to take responsibility away from the processes (besides the fact that it makes the lives of programmers easier) is because they cannot be trusted. However, we still seem to "trust" JavaScript code embedded in malicious web pages.
- We're using a programming language for all kinds of purposes for which it has not been designed. Moreover, we require solutions for these issues that are not part of the language, API or runtime, such as (ab)using the timer and relying on external libraries implementing patterns to combine asynchronous functions.
- Sometimes these new use cases of JavaScript are called innovation, but the fact that we use techniques that are over 30 years old (and intentionally not using better modern alternatives), does not really look like innovation to me IMHO.
So are solutions using JavaScript that bad? I see that there is practical use and value in it (e.g. the V8 runtime compiles to efficient native code and there is a big eco-system of JavaScript libraries), but dealing with concurrency like this is definitely something I consider a big drawback and a huge step backwards.
UPDATE: I have written a follow up blog post that covers promises.
No comments:
Post a Comment