Different ways to share data between Main thread and worker thread
There are several ways to share data between Worker Threads and the main thread in Node.js. Below are examples of the most common methods:
- Message Passing: This is the default way to communicate between the main thread and worker threads, where data is serialized and passed via
postMessageandon('message'). - SharedArrayBuffer: This allows memory to be shared directly between the main thread and workers.
- Using Node.js Streams: Data can be shared between the main thread and workers using streams.
Let’s go through examples of each approach.
1. Message Passing
Description: The main thread and worker threads communicate using the postMessage() method. Data is serialized and passed between threads as messages.
Main File (main.js)
const { Worker } = require('worker_threads');
// Create a worker
const worker = new Worker('./worker-message.js');
// Send data to the worker
worker.postMessage({ task: 'calculate', number: 10 });
// Receive data from the worker
worker.on('message', (result) => {
console.log('Main thread received:', result);
});
// Handle errors
worker.on('error', (err) => {
console.error('Worker error:', err);
});Worker File (worker-message.js)
const { parentPort } = require('worker_threads');
// Listen for messages from the main thread
parentPort.on('message', (data) => {
if (data.task === 'calculate') {
const result = data.number * 2; // Simple calculation
// Send the result back to the main thread
parentPort.postMessage({ result });
}
});How It Works:
- The main thread sends a message containing the task and data (in this case, a number to be doubled) to the worker.
- The worker thread listens for messages, performs the calculation, and sends the result back to the main thread.
2. Using SharedArrayBuffer
Description: You can use SharedArrayBuffer to share a block of memory between the main thread and workers, allowing them to directly access and manipulate the same data without message passing.
Main File (main.js)
const { Worker } = require('worker_threads');
// Create a SharedArrayBuffer to hold 5 integers
const sharedBuffer = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT * 5);
const sharedArray = new Int32Array(sharedBuffer);
// Initialize the shared array
for (let i = 0; i < sharedArray.length; i++) {
sharedArray[i] = i * 10;
}
console.log('Main thread - Initial shared array:', sharedArray);
// Start the worker and pass the shared buffer
const worker = new Worker('./worker-shared-buffer.js', { workerData: sharedBuffer });
worker.on('message', () => {
console.log('Main thread - Modified shared array:', sharedArray);
});Worker File (worker-shared-buffer.js)
const { workerData, parentPort } = require('worker_threads');
// Create an Int32Array based on the shared buffer
const sharedArray = new Int32Array(workerData);
// Modify the shared array
for (let i = 0; i < sharedArray.length; i++) {
sharedArray[i] += 5;
}
parentPort.postMessage('Modification complete');How It Works:
- The main thread creates a
SharedArrayBufferand initializes it with some values. - The worker thread accesses the shared buffer, modifies it, and both the main and worker threads see the same changes.
3. Using Node.js Streams
Description: You can use streams to pass data between the main thread and worker threads. Streams are useful for handling large data, such as file contents, without needing to load everything into memory at once.
Main File (main.js)
const { Worker } = require('worker_threads');
const { Readable } = require('stream');
// Create a stream to pass data to the worker
const dataStream = new Readable({
read() {}
});
// Create a worker
const worker = new Worker('./worker-stream.js');
// Pipe the stream to the worker
dataStream.pipe(worker.stdin);
// Send data through the stream
dataStream.push('Hello from the main thread!');
dataStream.push(null); // No more data
// Receive output from the worker
worker.stdout.on('data', (data) => {
console.log(`Main thread received: ${data.toString()}`);
});Worker File (worker-stream.js)
const { parentPort } = require('worker_threads');
// Receive data from stdin
process.stdin.on('data', (data) => {
// Process the data (in this case, just convert to uppercase)
const result = data.toString().toUpperCase();
// Send the result back to the main thread via stdout
process.stdout.write(result);
});How It Works:
- The main thread creates a readable stream and pipes data into the worker.
- The worker thread reads from
stdin, processes the data, and sends the result back to the main thread throughstdout.
4. Using Atomics with SharedArrayBuffer
Description: When using SharedArrayBuffer in scenarios where multiple threads are modifying shared memory, you may need synchronization primitives like Atomics to prevent race conditions and ensure consistency.
Main File (main.js)
const { Worker } = require('worker_threads');
// Create a SharedArrayBuffer to hold a single integer
const sharedBuffer = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT);
const sharedArray = new Int32Array(sharedBuffer);
// Initialize the shared value
sharedArray[0] = 0;
console.log('Main thread - Initial value:', sharedArray[0]);
// Start a worker and pass the shared buffer
const worker = new Worker('./worker-atomics.js', { workerData: sharedBuffer });
// Wait for the worker to finish
worker.on('exit', () => {
console.log('Main thread - Final value:', sharedArray[0]);
});Worker File (worker-atomics.js)
const { workerData } = require('worker_threads');
const sharedArray = new Int32Array(workerData);
// Increment the shared value atomically
for (let i = 0; i < 10000; i++) {
Atomics.add(sharedArray, 0, 1); // Safely add 1 to sharedArray[0]
}
// No need to send a message; main thread will see changes automaticallyHow It Works:
- Atomics.add() ensures that the shared value is incremented safely in a multi-threaded environment, preventing race conditions.
Summary:
- Message Passing: Data is serialized and passed as messages between the main thread and workers. Useful for isolated tasks where data doesn’t need to be shared.
- SharedArrayBuffer: Memory can be directly shared between the main thread and workers, allowing efficient data access and modification without message passing.
- Streams: Data can be passed using streams, which are useful for handling large data or continuous input/output.
- Atomics with SharedArrayBuffer: Allows safe modification of shared memory in a multi-threaded environment, preventing race conditions.
Each method has its use case depending on the nature of the task, how much data you need to pass, and whether synchronization is required.
