For a long time, my language of choice has been JavaScript (and recently TypeScript), this is for both frontend development as well as backend development.
I enjoy the simplicity of using the same language for both sides of a project and I especially like working with Node.js, a Javascript run time environment with a very sophisticated asynchronous event loop, which makes it perfect for applications such as RESTful APIs which access databases etc…
However, I have also done backend work using Golang, a strongly typed, compiled, very fast language with a vibrant community and a “do it yourself” attitude, quite the change from Javascript. And something I have touched and enjoyed playing with are go routines, which are a way to create very light weight threads. This works by calling a function as a “go routine”, which I think is very elegant. This is managed by the go run-time environment, but it can spawn threads if it needs to - a nice abstraction which means I don’t have to handle threads directly - but do have to handle go routines as if they ARE threads.
But anyways, it left me wondering if I could have threads in JavaScript, which sounds crazy but Node.js has support for this is “Worker Threads”. These are threads managed by Node.js which can run a .js file and communicate with its parent - it is quite similar to go routines.
So I made a small program to calculate the same MD5 hash 1 million times:
(Note that these files are .mjs files (module JavaScript), Node.js will only run them if you have .mjs as the file extension - All they do is allow the “import” syntax and a few other things).
import crypto from "crypto";
console.time("Single-Thread");
const n = 10000000;
const string = "Hello World";
for (let i = 0; i < n; i++) {
const hash = crypto.createHash("md5").update(string).digest("hex");
}
console.timeEnd("Single-Thread");
Running this program gave me this result:
Single-Thread: 1.139s
I then modified my code to use worker threads. This splits the work load into 8 worker threads, it gives each thread a start and an end range for the hash - which in total makes 1 million hashes.
import { fileURLToPath } from "url";
import { Worker, isMainThread, parentPort, workerData } from "worker_threads";
import crypto from "crypto";
const __filename = fileURLToPath(import.meta.url);
const n = 1000000;
const THREAD_NUM = 8;
const string = "Hello World";
if (isMainThread) {
const threads = new Set();
for (let i = 0; i < THREAD_NUM; i++) {
threads.add(
new Worker(__filename, {
workerData: {
start: i * (n / THREAD_NUM),
end: i * (n / THREAD_NUM) + n / THREAD_NUM,
},
}),
);
}
console.time("Thread");
for (const worker of threads) {
worker.on("message", (msg) => console.log(msg));
worker.on("exit", () => {
threads.delete(worker);
if (threads.size === 0) {
console.timeEnd("Thread");
}
});
}
} else {
for (let i = workerData.start; i < workerData.end; i++) {
const hash = crypto.createHash("md5").update(string).digest("hex");
}
parentPort.postMessage("Done!");
}
This results in the following output:
Done!
Done!
Done!
Done!
Done!
Done!
Done!
Done!
Thread: 335.294ms
A 4x increase in speed. There is a delay because worker threads actually spawn threads and therefore there is a bit of overhead with the syscall to create these threads.
This is very clear if we changed the number of hashes to 10 000.
Single Thread
Single-Thread: 23.476ms
8 Worker Threads
Done!
Done!
Done!
Done!
Done!
Done!
Done!
Done!
Thread: 70.337ms
As you can see the single thread beat the 8 threads by almost 4x. This is because it is costly to create a thread outright which is what Node.js seems to be doing.
Disclaimer
The Node.js crypto module is basically a wrapper for the native OpenSSL hashing functions, so I suspect that Node.js is just making a very fast C program do the hard work - but never the less it works very well.
Conclusion
Node.js is a very solid backend technology to perform parallalism, it is not just limited to making RESTful APIs. It can competes with strongly typed and compiled languages such as golang.