Multithreading is a tiny runtime that allows you to execute JavaScript functions on separate threads. It is designed to be as simple and fast as possible, and to be used in a similar way to regular functions.
With a minified size of only 4.5kb, it has first class support for Node.js, Deno and the browser. It can also be used with any framework or library such as React, Vue or Svelte.
Depending on the environment, it uses Worker Threads or Web Workers. In addition to ES6 generators to make multithreading as simple as possible.
JavaScript's single-threaded nature means that tasks are executed one after the other, leading to potential performance bottlenecks and underutilized CPU resources. While Web Workers and Worker Threads offer a way to offload tasks to separate threads, managing the state and communication between these threads is often complex and error-prone.
This project aims to solve these challenges by providing an intuitive Web Worker abstraction that mirrors the behavior of regular JavaScript functions. This way it feels like you're executing a regular function, but in reality, it's running in parallel on a separate threads.
npm install multithreading
import { threaded } from "multithreading";
const add = threaded(function* (a, b) {
return a + b;
});
console.log(await add(5, 10)); // 15
The add
function is executed on a separate thread, and the result is returned to the main thread when the function is done executing. Consecutive invocations will be automatically executed in parallel on separate threads.
import { threaded, $claim, $unclaim } from "multithreading";
const user = {
name: "john",
balance: 0,
};
const add = threaded(async function* (amount) {
yield user; // Add user to dependencies
await $claim(user); // Wait for write lock
user.balance += amount;
$unclaim(user); // Release write lock
});
await Promise.all([
add(5),
add(10),
]);
console.log(user.balance); // 15
This example shows how to use a shared state across multiple threads. It introduces the concepts of claiming and unclaiming write access using $claim
and $unclaim
. This is to ensure that only one thread can write to a shared state at a time.
Always
$unclaim()
a shared state after use, otherwise the write lock will never be released and other threads that want to write to this state will be blocked indefinitely.
The yield
statement is used to specify external dependencies, and must be defined at the top of the function.
import { threaded, $claim, $unclaim } from "multithreading";
// Some external function
function add (a, b) {
return a + b;
}
const user = {
name: "john",
balance: 0,
};
const addBalance = threaded(async function* (amount) {
yield user;
yield add; // Add external function to dependencies
await $claim(user);
user.balance = add(user.balance, amount);
$unclaim(user);
});
await Promise.all([
addBalance(5),
addBalance(10),
]);
console.log(user.balance); // 15
In this example, the add
function is used within the multithreaded addBalance
function. The yield
statement is used to declare external dependencies. You can think of it as a way to import external data, functions or even packages.
As with previous examples, the shared state is managed using $claim
and $unclaim
to guarantee proper synchronization and prevent data conflicts.
External functions like
add
cannot have external dependencies themselves. All variables and functions used by an external function must be declared within the function itself.
When using external modules, you can dynamically import them by using yield "some-package"
. This is useful when you want to use other packages within a threaded function.
import { threaded } from "multithreading";
const getId = threaded(function* () {
/**
* @type {import("uuid")}
*/
const { v4 } = yield "uuid"; // Import other package
return v4();
}
console.log(await getId()); // 1a107623-3052-4f61-aca9-9d9388fb2d81
You can also import external modules in a variety of other ways:
const { v4 } = yield "npm:uuid"; // Using npm specifier (available in Deno)
const { v4 } = yield "https://esm.sh/uuid"; // From CDN url (available in browser and Deno)
Errors inside threads are automatically injected with a pretty stack trace. This makes it easier to identify which line of your function caused the error, and in which thread the error occured.