JavaScript Async - DoEvents & Microtasks |
Written by Ian Elliot | |||
Monday, 19 August 2019 | |||
Page 2 of 2
ReentrancyA big problem of approaches like doEvents and hence to async and wait is that they provide no obvious protection against the reentrancy problem. Reentrancy is what it sounds like – it is when a thread enters a section of code that another thread is already executing. In a single-threaded environment this can only happen if you have an instruction such as await or doEvents which pauses the code and provides the opportunity to start it once again. In this case the same thread starts to execute the code it hasn’t finished. In most cases code is not written to be reentrant and the result is usually some unexpected behavior if not a system crash. The question is, what happens in JavaScript if a function is reentered? The answer is surprisingly simple – functions are largely reentrant in JavaScript. For example, consider the program in the previous section that computes Pi using doEvents to keep the UI responsive. Suppose the user clicks the button more than once. To make it easier to see what is going on change the doEvent function to read: function doEvents() { return pause(5000); } This makes it easier to see what is going on by inserting a longer pause. You might think that the result would be a mess as the for loop that is suspended by the await is restarted by the user clicking the button a second time. In fact the for loop isn’t restarted. Every function evocation in JavaScript starts the execution of the function off from the start, but each resume after the await restores all of the variables back to their original values. As this can only happen if the UI thread has been freed by the new invocation being paused by an await, the two invocations take turns running. This largely makes JavaScript reentrancy safe, but not completely so. Consider this version of computePi: state = {}; state.k = 0; state.pi = 0; async function computePi() { state.pi = 0; for (state.k = 1; state.k <= 10000000; state.k++) { state.pi += 4 * Math.pow(-1, state.k + 1) / The only change is that now the result is being stored in a global state object. If you try the same experiment what you will find is that now clicking the button a second time resets the computation back to zero. What is happening is that the global state object is being shared between all of the invocations of the computePi function. When a new invocations starts it zeros the properties of the state object and when the old invocation resumes it uses the same, newly zeroed, state object. Sharing global objects isn’t a good idea unless you have some special behavior in mind. If we change state to be a local variable: async function computePi() { var state = {}; state.k = 0; state.pi = 0; Then we return to the previous case where the object that state references is restored when the thread resumes after the await with the effect each time the user clicks the button another independent computation starts running. Worker ThreadsThere is nothing unique about the UI thread when it comes to async and await. You can define async functions in a Worker thread and await them in the usual way. Notice that in this case the event queue that is involved doesn’t have any events from the UI and this is a simplification. You can use the doEvents approach to periodically free up the Worker thread so that it can respond to any messages sent to it from another thread. For example we can implement the thread control pattern from Chapter 5 using doEvents: async function computePiAsync() { var k; for (k = 1; k <= 1000000; k++) { pi += 4 * Math.pow(-1, k + 1) / (2 * k - 1); if (Math.trunc(k / 1000) * 1000 === k) await doEvents(); } } function doEvents() { return pause(0); } async function pause(t) { var promise = new Promise( function (resolve, reject) { setTimeout( function () { resolve(); }, t); }); return promise; } Notice that this is essentially the program given earlier, but now we use await doEvents in computePiAsync to free the Worker thread. The message event handler is unchanged: var pi=0; this.addEventListener("message", function (event) { switch (event.data) { case "start": computePiAsync(); break; case "update": postMessage(pi); break; case "stop": close(); break; } });
To make use of the worker we need: var worker = new Worker("pi.js"); worker.addEventListener("message", function (event) { result.innerHTML = event.data; } ); worker.postMessage("start"); setInterval(function () { worker.postMessage("update"); }, 100); Notice that without the doEvents the update message would only be handled at the end of the computation. You need to include some code to stop the function being started again before it has finished and all of the considerations with reentrant code apply to Worker threads. Summary
Now Available as a Book:JavaScript AsyncYou can buy it from: Amazon Contents
Also by Ian Elliot To be informed about new articles on I Programmer, sign up for our weekly newsletter, subscribe to the RSS feed and follow us on Twitter, Facebook or Linkedin.
Comments
or email your comment to: comments@i-programmer.info <ASIN:1871962560> <ASIN:1871962579> <ASIN:1871962528> <ASIN:1871962501>
|
|||
Last Updated ( Monday, 19 August 2019 ) |