Parallelism in Javascript: A Quick Introduction

Reading Time: 5 minutes

JavaScript is a single-threaded language, which means it can only execute one task at a time. However, Web Workers and the JavaScript event loop enable parallelism, allowing for the execution of multiple tasks at the same time. Parallelism is important for improving performance, particularly for CPU-intensive or I/O-bound tasks, but it should be used with careful consideration of potential synchronization issues.

Intro

Code can be run in parallel within a Javascript environment, which is a good thing. Meaning you can do things like running a long process in the background and still have the browser responsive. We are not going to cover all the possibilities here, but we will cover the basics. First, we will go over the different types of constraints a program may have to execute in parallel, and we will use a network example to summarize the concepts in a simple way to use.

In a program there are a number of factors that can control the slowest part of the process. This is commonly known as bound. There are several bounds that can affect the performance of a program. The most common bounds are: CPU bound, I/O bound, and memory bound.

CPU Bound:

This is when the program is waiting for the CPU to finish a task. This is the most common bound. This is because the CPU is a computer’s most powerful part. The CPU can do a lot of things through task switching, however it can only do one thing at a time. So if the CPU is waiting for a task to finish, then the program is CPU bound.

The following code is an example of a CPU bound program. This program is bound by the CPU since it is waiting for the CPU to finish the task.

var start = new Date().getTime();
var end = start;
var array = []
while (end < start + 5000) {
    for (var i = 0; i < 1000000; i++) {
        array.push(i * i);
    }
}
end = new Date().getTime();
console.log("Time taken: " + (end - start) + "ms");

I/O Bound:

This is when the program is waiting for an I/O operation to finish. This is the second most common bound. This is because I/O operations are slower than CPU operations. So if the program is waiting for an I/O operation to finish, then the program is I/O bound.

The following code is an example of an I/O bound program. This program is bound by the I/O because it is waiting for the I/O operation to finish.

var start = new Date().getTime();
var end = start;

var request = new XMLHttpRequest();
request.open("GET", "http://www.example.com/resource1", true);
request.onreadystatechange = function() {
    if (request.readyState == 4) {
        console.log(request.responseText);
        end = new Date().getTime();
        console.log("Time taken: " + (end - start) + "ms");
    }
};
request.send(null);

Memory Bound:

This is when the program is waiting for memory to finish a task. This is the least common bound since memory is the slowest part of the computer. If the program is waiting for memory to finish a task, then the program is memory bound.

The following code is an example of a memory bound program.

var start = new Date().getTime();
var end = start;

var array = [];
for (var i = 0; i < 1000000; i++) {
    array.push(1);
}
end = new Date().getTime();
console.log("Time taken: " + (end - start) + "ms");

Applying The Knowledge

For the sake of simplicity we will only be talking about Network bound programs. These are programs that are bound by the network, as the network is the slowest part of the computer. So if the program is waiting for the network to finish a task, then the program is network bound.

The following code is an example of a network bound program. This is a simple example of using parallelism in javascript in the context of a browser.

If you have to request several resources, you can use this simple code to request a single resource at a time.

var resources = ["http://www.example.com/resource1", "http://www.example.com/resource2", "http://www.example.com/resource3"];
var start = new Date().getTime();
var end = start;

function requestResource(url) {
    return new Promise(function(resolve, reject) {
        var req = new XMLHttpRequest();
        req.open('GET', url);
        req.onload = function() {
            if (req.status == 200) {
                resolve(req.response);
            } else {
                reject(Error(req.statusText));
            }
        };
        req.onerror = function() {
            reject(Error("Network Error"));
        };
        req.send();
    });
}
requestResource(resources[0]).then(function(response) {
    requestResource(resources[1]).then(function(response) {
        requestResource(resources[2]).then(function(response) {
            end = new Date().getTime();
            console.log("Time taken: " + (end - start) + "ms");
        }, function(error) {
            console.log(error);
        });
    }, function(error) {
        console.log(error);
    });
}, function(error) {
    console.log(error);
});

The total amount of time to load the resources is the sum of all the times taken by each resource. But what if there was a way to load all the resources at the same time? This is where parallelism comes in.

var resources = [
    "http://www.example.com/resource1", 
    "http://www.example.com/resource2", 
    "http://www.example.com/resource3"
];
var start = new Date().getTime();
var end = start;

var promises = resources.map(function(url) {
    return new Promise(function(resolve, reject) {
        var req = new XMLHttpRequest();
        req.open('GET', url);
        req.onload = function() {
            if (req.status == 200) {
                resolve(req.response);
            } else {
                reject(Error(req.statusText));
            }
        };
        req.onerror = function() {
            reject(Error("Network Error"));
        };
        req.send();
    });
});

Promise.all(promises).then(function(responses) {
    console.log("All resources have been loaded");
    console.log(responses);
    end = new Date().getTime();
    console.log("Time taken: " + (end - start) + "ms");
}).catch(function(error) {
    console.log("An error has occurred");
    console.log(error);
});

Now, this is a simple case where we are using a common operation of loading resources from external resources. But not all the processes are like this, some are more complex and require more than one step. For example, you may have to load a resource, parse it, and then do something with the parsed data. But can we extend this to any number of requests? The answer is no; as with any other resource, there is a limit to the number of requests that can be made at the same time. This limit is called the concurrency limit. This limit is set by the browser. The concurrency limit is usually set to 6. This means that the browser can only make 6 requests at the same time. This is why we have to use a queue to make sure that we do not exceed the concurrency limit.

var resources = [
    "http://www.example.com/resource1", 
    "http://www.example.com/resource2", 
    "http://www.example.com/resource3", 
    "http://www.example.com/resource4", 
    "http://www.example.com/resource5", 
    "http://www.example.com/resource6", 
    "http://www.example.com/resource7", 
    "http://www.example.com/resource8", 
    "http://www.example.com/resource9", 
    "http://www.example.com/resource10", 
    "http://www.example.com/resource11", 
    "http://www.example.com/resource12"];
var start = new Date().getTime();
var end = start;

var promises = resources.map(function(url) {
    return new Promise(function(resolve, reject) {
        var req = new XMLHttpRequest();
        req.open('GET', url);
        req.onload = function() {
            if (req.status == 200) {
                resolve(req.response);
            } else {
                reject(Error(req.statusText));
            }
        };
        req.onerror = function() {
            reject(Error("Network Error"));
        };
        req.send();
    });
});

Promise.all(promises).then(function(responses) {
    console.log("All resources have been loaded");
    console.log(responses);
    end = new Date().getTime();
    console.log("Time taken: " + (end - start) + "ms");
}).catch(function(error) {
    console.log("An error has occurred");
    console.log(error);
});

Conclusion

Even though we did not cover all the possibilities to run code in parallel, the basic principles still apply, you can take advantage of parallelism to make your code run faster. But you have to be careful not to exceed the concurrency limit. If you do, you will end up with a slower program. So you have to be careful when using parallelism. If you use it correctly you can make your program run faster within the capacities of the environment and the constraints of your program.

Remember that early optimization is the root of all evil. So do not try to optimize your code before you have to; in case you have to, then make sure to first identify the bottlenecks in your code. Then you can use parallelism to make your code run faster.

0 Shares:
You May Also Like