Skip to main content

Introduction to Worker Threads with TypeScript

note

本文转载自 Node.js TypeScript

The Node.js and JavaScript are often perceived as a single-threaded but in this series, we prove that it is not entirely true. Node.js creates threads when performing Input/Output operations. Not only that, but we can also create additional processes. While the above does not count yet as multithreading, in this article we begin to cover the basics of TypeScript Worker Threads and explore how they work and compare them to other ways of running JavaScript in parallel that we’ve used so far.

Introduction to TypeScript Worker Threads

First, let’s look into why would we want another way of running multiple pieces of code at once. In the 10th part of this series, we cover the child process module. It allows us to create additional Node.js processes with all its advantages and disadvantages. Since child processes don’t share any memory, every one of them needs to manage their own environment to run Node.js code. Not only it uses quite a bit of memory: it also takes some time to get it to run. Threads, compared to multiple processes, are lighter when it comes to resources.

TypeScript support in Worker Threads

Even though the Worker Threads module is relatively new, it has solid TypeScript support when it comes to types. When we look into the DefinitelyTyped repository , all the use cases that we need in this article are covered. Moreover, in the history of the typings, you can see that they receive updates along with new versions of Node.js. It is very important because the Worker Threads module is still in the experimental stage. It means that it is still in active development and therefore its API can still change. The Node.js team seems to put quite a focus on Worker Threads though and lately, Node.js changelog often mentions Worker Threads. Before, we needed to run Node with an additional flag, —experimental-worker, but not anymore.

With the new Worker, we create a Worker in a similar way that we create a child process.

The only issue that we encounter is the lack of support for .ts files. When using the child process module, the new process inherits the process.execArgv property and therefore it runs with TypeScript. Currently, it is not the case with Worker Threads. Trying to run a .ts file in a worker thread results in error:

The worker script extension must be “.js” or “.mjs”. Received “.ts”

As of now, the only way to overcome this issue is to import .js files. There are a few approaches that we can take with this problem, for example creating a .js file for every worker that we want to use as suggested on GitHub:

worker.js
const path = require('path');

require('ts-node').register();
require(path.resolve(__dirname, './worker.ts'));

Thanks to using require('ts-node').register(), the worker.ts file is compiled to TypeScript.

The above means quite a lot of redundant code because we need to create an additional .js file for every worker. We can improve by passing a file name to the worker.js file using workerData.

Communicating between threads

We can use the workerData parameter to send data to the newly created worker. It can be any value that can be cloned by the structured clone algorithm – the workers receive cloned value. The above puts a few constraints on what we pass as the workerData. For example, we can’t pass functions to it. Let’s use it to give the path of our .ts file to worker.js.

main.ts
import { Worker } from 'worker_threads';

const worker = new Worker('./worker.js', {
workerData: {
path: './worker.ts'
}
});

When you are inside of a worker, you can read the workerData by importing it from the Worker Threads module. Let’s use it to resolve our .ts file.

worker.js
const path = require('path');
const { workerData } = require('worker_threads');

require('ts-node').register();
require(path.resolve(__dirname, workerData.path));

Thanks to the code above we have just one .js file that imports any .ts file so that we can use TypeScript with Worker Threads easily.

Since we got that down, let’s implement a way for the main thread and our worker thread to communicate further.

Communicating with the parent thread

The easiest way to communicate the worker thread with the main thread is to use parentPort that can be imported inside of a worker. You can send any data as long as it is compatible with the HTML structured clone algorithm.

worker.ts
import { parentPort, workerData } from 'worker_threads';

function factorial(n: number): number {
if(n === 1 || n === 0){
return 1;
}
return factorial(n - 1) * n;
}

parentPort.postMessage(
factorial(workerData.value)
);

In the code above we use the postMessage function to send data back to the main thread. A way to receive it is to listen for the message event:

main.ts
import { Worker } from 'worker_threads';

const worker = new Worker('./worker.js', {
workerData: {
value: 15,
path: './worker.ts'
}
});

worker.on('message', (result) => {
console.log(result);
});
> 1307674368000

And thanks to the code above we have our main thread communicating with the worker that is written in TypeScript!

Summary

In this article, we’ve covered the very basics of using TypeScript Worker Threads. To do this, we first had to figure out how to make them work with TypeScript. We also used the parentPort to communicate our worker thread with the main thread, therefore making our code fully functional. In the upcoming article, we dive deeper into this topic by creating our own ports using the MessageChannel and share the data between threads.