Parallelism in JavaScript
JavaScript engines are single-threaded:
-
All code runs in a single thread, in a single process,
- In old browsers: one process for the whole browser,
- In modern browsers: one tab = one process.
-
No native way to do background work.
-
All asynchronism is implemented through an event loop:
while True: # Fictive implementation of an event loop while queue.is_empty(): wait() task = queue.pop() task.execute()
-
Two types of function calls:
- Blocking: they execute immediately, block the thread until they are done,
- Non-blocking: they yield control to the event loop, queue a task when they are done.
Function calls: blocking vs non-blocking
Blocking calls
Most function calls:
- User defined functions,
- Computations:
Math
, string manipulations, array/object manipulations, … - DOM API:
.querySelector()
,.appendChild
, … eval
,require
,console.log
, …- …
Non-blocking calls
- Sleeping:
setTimeout()
,setInterval()
, - Input/Output:
- System: communicating with other processes (e.g., shell commands), …
- Disk: reading/writing files, reading/executing templates, …
- Network: reading/writing on sockets, HTTP requests/responses,
XMLHttpRequest
,fetch()
,app.listen()
, … - Databases: connecting to databases, querying databases,
knex.raw()
,knex.select()
, …
Handling asynchronous calls: callbacks
Classic technique, typical of event-based programming
function callback() { // (click to run)
alert('Hello');
}
setTimeout(callback, 2000); // Run callback in 2 secs
alert('world');
setTimeout
queuescallback
, to be executed in 2 seconds,- Control goes to the next instruction,
- After 2 seconds,
callback
is executed.
Also seen in event handlers, request handlers:
// Call when a click happens
div.addEventListener('click',
function(e) {
console.log('Hello click');
});
// Call when a HTTP request happens
app.get('/toto',
function(req, res) {
res.send("Hello world");
});
The problem with callbacks: Callback hell
app.get('/foo', function(req, res) {
setTimeout(function() {
knex.raw('SELECT * FROM users').asCallback(function(err, result) {
if (err)
console.log(err);
else if (result.length > 0) {
knex.raw(...).asCallback(...)
}
});
}, 2000);
});
This style is error prone:
- Code flow hard to follow,
- Errors difficult to catch,
- Very painful to write loops:
- exercise: write “Hello world” to the console every 2
seconds, without using
setInterval
.
- exercise: write “Hello world” to the console every 2
seconds, without using
- Hard to synchronize:
- waiting for the end of two or more actions,
- waiting for the end of the first among two or more actions.
Promises and async/await
Modern API and new syntax (only since ES2017) to handle asynchronous code
async function get_readme() {
try {
var response = await fetch('/README.md'); // wait for HTTP response
var content = await response.text(); // wait for end of response body
alert(content);
} catch (err) { // asynchronous errors are catched too
console.error(err);
}
}
get_readme();
- Non-blocking functions (e.g.,
fecth()
) return Promises, await
blocks execution until the promise is done,- when the promise is done
await
passes on the promised value.
Memorandum: long story short
- Always know which calls are non-blocking:
- Local input/output;
- Network requests (e.g.,
fetch()
); - Database queries (e.g.,
knex.raw()
,knex.from()
, …); - …
-
Always use
await
in front of non-blocking calls. -
Always wrap
await
inasync
function. - Don’t forget to catch errors with
try ... catch
.
Sadly: most failures to do so will result in subtle, not-so-easy-to-read, errors!
Ok. Now to the hardcore stuff!
What on earth is a Promise?!
async/await
is just syntactic sugar for:
var p1 = fetch('/README.md');
var p2 = p1.then(function(response) {
return response.text();
});
var p3 = p2.then(function(content) {
console.log(content);
});
var p4 = p3.catch(function(err) {
console.error(err);
});
console.log(p1, p2, p3, p4);
p1
,p2
,p3
,p4
are all Promises;- A promise is a sort of box protecting a value;
- You (the programmer) are only allowed to act on the value by passing callbacks.
Promises are boxes
Chain promises
This style is more succint, and preferred:
fetch('/README.md')
.then(function(response) {
return response.text();
})
.then(function(content) {
alert(content);
})
.catch(function(err) {
console.error(err);
});
Good to know:
- JavaScript automatically decapsulates nested promises (e.g.,
response.text()
returns a promise, the boxed value is passed to the next callback). - Errors in the callbacks are passed to
.catch()
. - An error not handled by a
.catch()
will usually crash the application.
Careful with naked promises
Racing promises
var start = Date.now()
function cb(response) {
var elapsed = Date.now() - start;
alert(`${response.url} (${elapsed}ms)`);
}
fetch('/index.html').then(cb); // these two execute in parallel
fetch('/README.md').then(cb);
If you want to order requests, you must chain callbacks (or use async/await
)
var start = Date.now()
fetch('/index.html')
.then(function (response) {
alert(`${response.url} (${Date.now() - start}ms)`);
return fetch('/README.md'); // fetch 2nd only after 1st is done
})
.then(function (response) {
alert(`${response.url} (${Date.now() - start}ms)`);
});
Advanced parallelism
Promise.all([ // these two execute in parallel
fetch('/index.html'),
fetch('/README.md'),
]).then(function(responses) {
alert(responses[0].url + ", "
+ responses[1].url);
return fetch('/'); // this one executes after
}).then(function(response) {
alert(response.url);
});
- Create your own promises using
new Promise()
, - Advanced parallelism using the
Promise
inferface, - Read more on MDN.
References
- Eloquent JavaScript, Chapter 17 on promises,
- MDN docs on
async/await
, - MDN docs on using Promises.
- Article on promises,
- Article on the fetch API.
- An interactive visualisation of the event queue: http://latentflip.com/loupe/