At first glance, parallel processing sounds like an invitation to free lunch – being able to use multi-core CPUs more efficiently should give your code a tremendous speed boost. Practical experience shows that this is not always the case: some problems are impossible to parallelise. In addition to that, parallel execution leads to an entire family of new problems that are not seen on single-core machines.
Parallel code can, in principle, be broken down into two groups of job. The first group is classic speed increase – if your program has to sift through 3000 images, splitting the job up means that more images can be processed at any one time. While definitely beneficial, this kind of task is rarely encountered in everyday web development. (If you're creating an image-heavy site, make sure you back them up with decent cloud storage).
The more common form of parallelisation job works around long-running tasks which do not require much CPU time. A good example for this would be waiting for a download or some kind of device input: if this is done in a parallelised fashion, the rest of the GUI does not need to be blocked. Given that users tend to have issues understanding 'why the button doesn’t click', this can lead to increased user satisfaction even if actual speed does not increase.
If you need some new resources to help you code smarter, check out our guide to the best web design tools of 2019. If you want to avoid the code altogether, design your site with the best website builder, and ensure you've chosen the perfect web hosting service with our guide.
You might also want to see our selection of CSS and JavaScript tutorials, the best JavaScript frameworks, and these top JavaScript tools.
01. Intervalled execution
var sleep = require('sleep');
function worker(){
console.log("Worker up")
sleep.msleep(1000)
console.log("Worker down")
}
setInterval(worker, 2000);
while(1==1)
{
sleep.msleep(250)
console.log("Hello from main")
}>
Let us start out with a small example based on a well-liked favourite: the setInterval function. It takes a function reference and a numeric value delineated in milliseconds. After that, the function is periodically fired up whenever the delays timer expires.
02. Setting up for a fall
npm install sleep
npm example1.js
Using the sleep() function requires us to load the sleep module to a local npm project. It acts as an interface to the operating system’s sleep library – do not wonder if your workstation’s compiler is fired up during the deployment of the package.
Get the Creative Bloq Newsletter
Daily design news, reviews, how-tos and more, as picked by the editors.
03. Perils of multitasking
When running this program, you will find yourself confronted with output similar to the one shown in the figure accompanying this step. It is obvious that our worker never gets invoked – something must be wrong with the setInterval function.
04. Perils of multitasking, part 2
Modern browsers equip each tab with one JavaScript thread. A careful look at our loop reveals that it is endless. This means that it will run forever, and will never yield control. Given that setInterval() works with a message, its payload never gets run as the message handler is blocked from running.
05. Focus on troublemakers
const bar = () => console.log('bar')
const baz = () => console.log('baz')
const foo = () => {
console.log('foo')
setTimeout(bar, 0)
baz()
}
foo()
Flavio Copes' blog, provides an extremely interesting bit of code illustrating the problem. When run, it yields the output shown in the figure due to the message queue being blocked until foo() relinquishes control of the main thread.
06. Introduce threading
Given that the underlying operating system is able to perform preemptive multitasking, let us harness its capabilities by spawning threads via a dedicated API called WebWorkers, which enjoy wide-ranging support. The figure accompanying this step shows the current listing for the feature.
07. Create an extra file
console.log("Worker says hello!");
console.log("Worker up");
while (1==1)
{
console.log("x");
}
WebWorkers cannot kick off with a function payload. Instead, a new file is required which contains the code intended to run in the thread. In the case of our example, example7thread.js has the content shown accompanying this step.
08. Run the worker...
const worker = require('worker_threads');
var myWorker = new worker.Worker('./
example7thread.js');
var myWorker2 = new worker.Worker('./
example7thread.js');
Our worker is ready for prime-time. Specialities of the Node.JS runtime force us to include the worker threads module and pass in a relative path – problems not faced in the browser. Furthermore, do not wonder about the missing start() call – a web worker sets off when the instance comes online.
09. ...and understand its results
Message delivery usually requires some kind of interaction between the runtime and the rest of the operating system. Sadly, our endless loop blocks this process – which means that only one of the messages pops up before 'the big stall'.
10. Add the mask
const worker = require('worker_threads');
var myWorker = new worker.Worker('./
example7thread.js');
var myWorker2 = new worker.Worker('./
example7thread.js');
while (1==1)
{
}
Another experiment involves placing an empty loop after the two constructor invocations. In this case, the threads will never start working – the invocation message never arrives with the operating system.
11. Interprocess communication
While workers running tight endless loops do have the problems exhibited above, routines can communicate with one another. This is done via a message passing interface – think of it as a facility which lets you pass a message object from a sender to a recipient across thread boundaries.
12. Why message-pass?
In addition to the benefits of being able to coordinate threads efficiently (and reduced risk of 'collisions', also known as race conditions), message passing to the main thread is the only way to interact with the DOM, due to the difficulty of creating thread-safe GUI stacks.
13. Move to the browser
Implementing message passing in Node.JS is tedious. Move the code to the web – start out by creating a HTML harness that loads examplempi.js. Be aware that this code can only be run from a localhost web server due to DOM origin restrictions.
14. Set up a mailbox...
var myWorker = new Worker('./
examplempithread.js');
var myWorker2 = new Worker('./
examplempithread.js');
myWorker.onmessage=function (e)
{
console.log(e);
}
myWorker2.onmessage=function (e)
{
console.log(e);
}
Each worker exposes an onmessage property which takes up a function reference that must be invoked whenever a message pops up from the other end. Incoming messages are simply forwarded to the console of the browser from the main thread.
15. ...and feed it from the worker
postMessage("Worker says hello!");
postMessage("Worker up");
while (1==1)
{
postMessage("x");
}
Sending messages to a mailbox is accomplished by invoking the post Message function. Keep in mind that a worker can also implement an onmessage event, which could receive information from whoever 'owns' the worker instance object.
16. Kick it off...
At this point in time, the code is ready to run. The developer console isn’t flooded with messages due to the high efficiency of the MPI interface, meaning data can travel around the system efficiently.
17. Async and await
Microsoft’s addition of the async and await keywords modified the history of C# and VisualBasic.Net. In principle, a method marked async is run cooperatively with the rest of the program. Results can then be harvested via the await function.
18. Replace our sleeper
const sleep = (milliseconds) => {
return new Promise(resolve =>
setTimeout(resolve, milliseconds))
}
Given that browsers are under severe limitations when it comes to accessing native functions, we cannot reuse the sleep function mentioned before. It, furthermore, is suboptimal in that it halted the entire Node.JS runtime. The snippet accompanying this step provides a more effective approach.
19. Understand the code...
This code returns a promise object – it is another convenience class added to Java Script in an attempt to make multithreading easier. You will need to invoke its resolve method via a setTimeout, ensuring that the code has to 'sit out' quite a bit of time.
20. ...and deploy it somewhere
Await calls are permitted only inside of async functions. This means that the worker is in need of a rewrite – start off by invoking an async bearer function. It handles the rest of the code interaction.
const sleep = (milliseconds) => {
return new Promise(resolve =>
setTimeout(resolve, milliseconds))
}
postMessage("Worker says hello!");
postMessage("Worker up");
worker()
async function worker(){
while (1==1)
{
postMessage("x");
await sleep(1000);
}
}
21. Harness the power of native!
Developers working on Node.JS code should not forget that they can – usually – also create completely native modules if extremely high performance is needed. These can not only take advantage of the various operating system APIs, but can also be written in programming languages that compile to machine code.
This article was originally published in issue 291 of creative web design magazine Web Designer. Buy issue 291.
Read more:
Thank you for reading 5 articles this month* Join now for unlimited access
Enjoy your first month for just £1 / $1 / €1
*Read 5 free articles per month without a subscription
Join now for unlimited access
Try first month for just £1 / $1 / €1
Tam Hanna is a software consultant who specialises in the management of carrier and device manufacturer relationships, mobile application distribution and development, design and prototyping of process computers and sensors. He was a regular contributor to Web Designer magazine in previous years, and now occupies his time as the owner of Tamoggemon Software and Computer Software.