Threaded JavaScript!

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.

John Costa

Software Engineer


By John Costa, 2022-04-10


On this page: