JavaScript is a single-threaded language, meaning tasks execute one at a time on the main thread. While this design simplifies development, it can lead to performance bottlenecks for computationally heavy tasks. This blog explores how Web Workers, SharedArrayBuffer, and Atomics can enable multithreading in JavaScript to build high-performance applications.
Why Use Web Workers, SharedArrayBuffer, and Atomics?
Web Workers
Web Workers run JavaScript in background threads, preventing intensive tasks from blocking user interactions like scrolling or button clicks.
SharedArrayBuffer
SharedArrayBuffer allows memory to be shared between the main thread and workers without copying, enabling faster communication.
Atomics
Atomics ensure safe and synchronized access to shared memory, preventing race conditions and maintaining data consistency across threads.
Example: A Real-World Task with Web Workers and SharedArrayBuffer
Let’s implement a simple and real-world example: calculating the sum of a large array in parallel.
Step 1: Creating a Web Worker Script
Create a file named worker.js to handle partial sum calculations:
// worker.js
self.onmessage = function (event) {
const { array, start, end } = event.data;
let sum = 0;
for (let i = start; i < end; i++) {
sum += array[i];
}
self.postMessage(sum);
};
Step 2: Setting Up the Main Thread
In the main script, divide the task among workers.
// main.js
const array = Array.from({ length: 1_000_000 }, () => Math.floor(Math.random() * 100));
const numWorkers = 4;
const chunkSize = Math.ceil(array.length / numWorkers);
const workers = [];
const results = [];
let completedWorkers = 0;
// Create a SharedArrayBuffer for the array
const sharedBuffer = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT * array.length);
const sharedArray = new Int32Array(sharedBuffer);
sharedArray.set(array);
// Initialize workers
for (let i = 0; i < numWorkers; i++) {
const worker = new Worker('worker.js');
workers.push(worker);
const start = i * chunkSize;
const end = Math.min(start + chunkSize, array.length);
worker.postMessage({ array: sharedArray, start, end });
worker.onmessage = function (event) {
results[i] = event.data;
completedWorkers++;
if (completedWorkers === numWorkers) {
const totalSum = results.reduce((acc, curr) => acc + curr, 0);
console.log('Total Sum:', totalSum);
}
};
}
Step 3: Using Atomics for Synchronization
Use Atomics to manage progress or ensure all threads are done before proceeding.
const progress = new Int32Array(sharedBuffer);
Atomics.add(progress, 0, 1); // Increment progress
if (Atomics.load(progress, 0) === numWorkers) {
console.log('All workers completed their tasks.');
}
Benefits of This Approach
Smooth User Experience: Offloads computation from the main thread.
Faster Communication: SharedArrayBuffer avoids data copying between threads.
Thread Safety: Atomics provide tools to handle synchronization effectively.
Real-World Use Cases
Real-Time Analytics: Process large datasets in parallel for faster insights.
Gaming Engines: Perform physics simulations in separate threads.
Media Processing: Encode or decode video streams without UI lag.
References
Top comments (0)