Concurrency and Multithreading in TypeScript: from Theory to Practice
Modern web applications increasingly face the need to process large volumes of data and perform resource-intensive operations without blocking the user interface. TypeScript, being a superset of JavaScript, inherits its single-threaded execution model, but at the same time provides powerful tools for organizing concurrency and multithreading. In this article, we will analyze how to effectively use these capabilities in your projects.
Understanding Concurrency and Multithreading
Before diving into the code, it is important to distinguish between two key concepts:
- Concurrency — the ability of a system to handle multiple tasks in overlapping time intervals, but not necessarily simultaneously. This is achieved through fast context switching.
- Multithreading — parallel execution of multiple threads on different processor cores. In Node.js, this is implemented via Worker Threads.
TypeScript runs in the Node.js environment, where the main thread processes events through the Event Loop. For heavy computations, this can become a bottleneck.
Asynchrony and Promise: the Foundation of Concurrency
The basic level of concurrency in TypeScript is built on async/await and Promise. These mechanisms allow you to avoid blocking the main thread while waiting for I/O operations.
// Example of asynchronous data loadinginterface User { id: number; name: string;}
async function fetchUser(id: number): Promise<User> { const response = await fetch(`https://api.example.com/users/${id}`); return response.json();}
// Parallel execution of multiple requestsasync function loadUsers(): Promise<User[]> { const userIds = [1, 2, 3, 4, 5]; const promises = userIds.map(id => fetchUser(id)); return Promise.all(promises);}
loadUsers().then(users => { console.log('Users loaded:', users.length);});Promise.all starts all promises simultaneously, but they execute in a single thread. This is ideal for I/O tasks, but not suitable for CPU-intensive operations.
Worker Threads: true Multithreading
To perform heavy computations without blocking the main thread, use Worker Threads. This is a built-in Node.js module that creates separate threads with their own memory.
Let's create an example of calculating Fibonacci numbers in a separate thread:
// fibonacci.worker.tsimport { parentPort } from 'worker_threads';
function fibonacci(n: number): number { if (n <= 1) return n; return fibonacci(n - 1) + fibonacci(n - 2);}
parentPort?.on('message', (n: number) => { const result = fibonacci(n); parentPort?.postMessage(result);});// main.tsimport { Worker } from 'worker_threads';import path from 'path';
function runFibonacciInWorker(n: number): Promise<number> { return new Promise((resolve, reject) => { const worker = new Worker(path.resolve(__dirname, 'fibonacci.worker.ts')); worker.postMessage(n); worker.on('message', resolve); worker.on('error', reject); worker.on('exit', (code) => { if (code !== 0) reject(new Error(`Worker stopped with exit code ${code}`)); }); });}
async function main() { console.time('fibonacci'); const result = await runFibonacciInWorker(40); console.timeEnd('fibonacci'); console.log(`Fibonacci(40) = ${result}`);}
main();Note: to run TypeScript workers, compilation to JavaScript is required. You can use ts-node or pre-compilation.
SharedArrayBuffer: shared memory between threads
When threads need to exchange large amounts of da