Threads and Workers

Although Javascript is important, it is not the only thing that is running in a browser. You have many processes that are running in the background to power the browser, for example the browser process, the renderer process, network process and many more. (See here for more). The process that I am going to focus on is the renderer process which contains an instance of V8 and is the process that runs our Javascript code. But before that, let's introduce a few terms.

Programs, Processes and Threads

A program can refer to any piece of software you have on your laptop, whether it is photoshop, chrome, or some python codes that you have written for your assignments. When we run a program, we spawn an instance of the program which is called process. For example, whenever you start chrome or any application, one or more processes will be created and you can see them in the task manager. A process takes up memory and will be terminated once you close the program.

When we look inside a process, you will find one or more threads where you codes are being run on. You will often hear what is called a main thread which is where the code starts to execute. From threads and processes can then be created and they can communicate with each other to exchange data. Threads inside of the same process share the same memory spaces which is both a good thing and bad thing. (Here is a Medium article about threads and processes if you want to learn more.)

Multithreading - From https://www.geeksforgeeks.org/multithreading-python-set-1/

Multithreading is where a process have multiple running threads and you can execute multiple tasks at the same time. Since the threads inside the same process share a common memory space, it is less resource intensive to create a new thread than a new process.

Multiprocessing on the other hand is where you have multiple processes running at the same time on different CPU cores. Each process occupies their own memory space and can communicate with another process via Inter Process Communication (IPC).

Concurrency and Parallelism are two concepts about the tasks that are being run on threads. Quoting from here, concurrency is when two or more tasks can start, run, and complete in overlapping time and parallelism is when tasks literally run at the same time, e.g., on a multicore processor.

Concurrency
Parallelism

Renderer Process

If you want to learn in-depth about the renderer process, I highly recommend going through this article which details the inner workings of the renderer process.

Generally when you open a new tab on chrome, a new renderer process gets created. It is responsible to turn HTML, CSS and Javascript into a web page that the user sees and interacts with. There are four type of threads inside of a renderer process: the main thread, the compositor thread, raster thread and worker threads. There can be multiple worker threads but there is only one main thread.

The main thread is responsible for many things: parsing files, calculating styles, layouts and creates paints record. It then feeds information from these steps to the compositor thread and raster thread to produce the final visible page.

Another thing that runs on the main thread is the V8 engine, which parses and executes our Javascript code. As mentioned, only one task can run on a single thread, so if you have too much processing going on, the steps for rendering cannot run and the screen would freeze until the main thread is available again.

Since the compositor and raster are running on separate threads, some CSS properties such as transform and opacity which doesn't require the main thread to process can give smooth animations when updated. (See this post for more)

Worker Threads

Apart from the main thread, compositor thread and raster thread, you can also create worker threads to offload some computational intensive tasks so you don't block the main thread. In browsers, you can run

const myWorker = new Worker("worker.js");

to create a new worker and use

myWorker.postMessage([first.value, second.value]);

to communicate with the worker. In the worker, you can then listen for messages and perform tasks:

onmessage = (e) => {
  console.log("Message received from main script");
  const workerResult = `Result: ${e.data[0] * e.data[1]}`;
  console.log("Posting message back to main script");
  postMessage(workerResult);
};

Similarly on the main thread, you can use onmessage to listen for messages from the workers:

myWorker.onmessage = (e) => {
  result.textContent = e.data;
  console.log("Message received from worker");
};

One of the limitations that workers have is that they do not have access to the DOM nor the window object since they run in a different context. (See this article for more)

You can also create worker threads in other runtimes (but the inner working would be different).

In Node JS, you can require worker threads by doing: (see this post)

const { Worker } = require('worker_threads');
const worker = new Worker('./worker.js', { workerData: { num: 5 }});
worker.on('message', (result) => {
  console.log('square of 5 is :', result);
})

In Bun, you can do:

const workerURL = new URL("worker.js", import.meta.url).href;
const worker = new Worker(workerURL);

worker.postMessage("hello");
worker.onmessage = event => {
  console.log(event.data);
};

There are a lot of considerations when implementing multithreading in Javascript. When you want to communicate between threads, you would either need to clone all the data passed, which is be inefficient with large arrays, or to use shared data that is in the form of a SharedArray buffer. If you use shared data, it can cause race conditions and synchronization problems which we need other ways to deal with such as Atomics.

 

Last edited on 17/04/2024