Understanding JS Parallel Tasks in One Article
Preface
In development, most scenarios involve sequential code execution,
but some scenarios require running multiple tasks in parallel.
This article explores how to efficiently execute parallel tasks.
Parallel Scenarios
Common scenarios for parallel task execution:
-
Running multiple requests in parallel
-
Downloading files in parallel
Parallel Tasks
The common characteristics of tasks in the above scenarios:
-
Multiple tasks have no dependencies on each other
-
The completion time of each task is uncertain
-
All tasks should complete as fast as possible
So we can use setTimeout to simulate parallel tasks,
where timeout can be passed in to simulate tasks with different durations:
/**
* handler
* @param {*} timeout
* @returns
*/
module.exports = function (timeout) {
return new Promise(function (resolve) {
setTimeout(() => {
return resolve(timeout);
}, timeout);
});
};
Task Pool
The task pool here is simply a collection of timeout values:
/**
* values
*/
module.exports = [100, 300, 200, 400];
Callbacks and Completion
When executing parallel tasks, we generally want:
-
A callback for each task when it completes
-
A complete callback when all tasks finish
The callback code is as follows:
// q
const q = require('qiao-console');
/**
* callback
* @param {*} index
* @param {*} total
* @param {*} res
*/
module.exports = function (index, total, res) {
q.writeLine(index, total, `task ${res} done`);
};
The complete code is as follows:
// q
const q = require('qiao-console');
/**
* complete
* @param {*} res
*/
module.exports = function (res) {
q.clear();
console.log('all tasks done');
console.log(res);
};
Serial Execution
First, let’s look at serial execution as a baseline.
Serial execution means tasks run one after another, and the total time is the sum of all task durations.
// q
const q = require('qiao-console');
// vars
const values = require('./_values.js');
const handler = require('./_handler.js');
const callback = require('./_callback.js');
const complete = require('./_complete.js');
// test
(async function () {
q.clear();
const results = [];
for (let i = 0; i < values.length; i++) {
const res = await handler(values[i]);
callback(i, values.length, res);
results.push(res);
}
complete(results);
})();
Parallel Execution (IIFE)
Using IIFE (Immediately Invoked Function Expression) to execute tasks in parallel.
The key idea is to start all tasks at once without awaiting each one sequentially.
// q
const q = require('qiao-console');
// vars
const values = require('./_values.js');
const handler = require('./_handler.js');
const callback = require('./_callback.js');
const complete = require('./_complete.js');
// test
(function () {
q.clear();
let doneCount = 0;
const results = [];
for (let i = 0; i < values.length; i++) {
(async function (index) {
const res = await handler(values[index]);
callback(index, values.length, res);
results[index] = res;
doneCount++;
if (doneCount === values.length) {
complete(results);
}
})(i);
}
})();
For an introduction to IIFE, see: https://developer.mozilla.org/zh-CN/docs/Glossary/IIFE
The core code after modification:
console.time('qiao-parallel');
for (let i = 0; i < values.length; i++) {
(async function (i, value) {
const res = await handler(value);
callback(i, res);
if (i + 1 == values.length) {
complete(values.length);
console.timeEnd('qiao-parallel');
}
})(i, values[i]);
}
Total runtime: 400ms (max(100, 200, 300, 400))

Parallel Execution (Multi-process)
In Node.js, you can also use multi-process to execute tasks in parallel.
Multi-process task execution uses Node.js’s child_process module.
Usage:
-
Prepare a JS file for the task
-
Fork to execute
-
Communicate via process
For child_process fork usage, see:
The fork code looks roughly like this:
const cp = child_process.fork(jsPath, args || []);
cp.on('message', function (msg) {
if (onMsg) onMsg(msg);
});
cp.on('exit', function (code) {
if (onExit) onExit(code);
});
The handler.js file needs to be modified for fork mode:
// handler
const handler = require('./_handler.js');
// fork handler
async function forkHandler() {
// check
if (!process || !process.argv) return;
// value
const value = parseInt(process.argv[2]);
// msg
const msg = await handler(value);
process.send(msg);
}
forkHandler();
Total runtime: 447ms (max(100, 200, 300, 400) + 47ms)

The runtime is less than serial’s 1s, but more than IIFE’s 400ms.
The extra 47ms can be attributed to process creation and destruction overhead.
Changing the values from 100-400ms to 1-4s to see the effect:
IIFE execution result: total time is still 4s (max(1, 2, 3, 4))

Fork execution result: total time is 4.045s (max(1, 2, 3, 4) + 45ms)

This proves that in multi-process parallel execution,
process creation and destruction takes roughly 40-50ms.
Parallel Execution (Multi-thread)
Node.js introduced the Worker threads module starting from v10.5.0.
See: https://nodejs.org/dist/latest-v18.x/docs/api/worker_threads.html#worker-threads
The worker code is as follows:
const worker = new Worker(jsPath, {
workerData: value,
});
worker.on('message', function (res) {
onCallback(callback, index, res);
});
worker.on('error', function (error) {
console.log(error);
});
worker.on('exit', function () {
onComplete(complete, valuesLength);
});
The corresponding handler code is modified to:
// worker
const { parentPort, workerData } = require('node:worker_threads');
// handler
const handler = require('./_handler.js');
// fork handler
async function forkHandler() {
// msg
var msg = await handler(workerData);
parentPort.postMessage(msg);
}
forkHandler();
Total runtime: 447ms (max(100, 200, 300, 400) + 34ms)

After changing to 1-4s, total runtime: 4.032s (max(1, 2, 3, 4) + 32ms)

The multi-thread overhead is around 30ms,
which is slightly faster than the 40-50ms of multi-process mode.
qiao-parallel
A wrapper npm package, feel free to use it: https://code.insistime.com/#/qiao-parallel
|— Wraps IIFE parallel task execution
// q
const q = require('qiao-console');
// vars
const values = require('./_values.js');
const handler = require('./_handler.js');
const callback = require('./_callback.js');
const complete = require('./_complete.js');
// parallel
const parallel = require('qiao-parallel');
// test
(function () {
q.clear();
parallel.parallelByIIFE(handler, values, callback, complete);
})();
|— Wraps multi-process parallel task execution
// q
const q = require('qiao-console');
// vars
const values = require('./_values.js');
const callback = require('./_callback.js');
const complete = require('./_complete.js');
// parallel
const parallel = require('qiao-parallel');
// test
(function () {
q.clear();
const jsPath = require('path').resolve(__dirname, './fork-handler.js');
parallel.parallelByFork(jsPath, values, callback, complete);
})();
|— Wraps multi-thread parallel task execution
// q
const q = require('qiao-console');
// vars
const values = require('./_values.js');
const callback = require('./_callback.js');
const complete = require('./_complete.js');
// parallel
const parallel = require('qiao-parallel');
// test
(function () {
q.clear();
const jsPath = require('path').resolve(__dirname, './worker-handler.js');
parallel.parallelByWorker(jsPath, values, callback, complete);
})();
Summary
-
Parallel tasks - Scenario introduction
-
Parallel tasks - Task characteristics
-
Parallel tasks - Task pool
-
Parallel tasks - Callbacks
-
Parallel tasks - IIFE pattern
-
Parallel tasks - Multi-process pattern
-
Parallel tasks - Multi-thread pattern
-
Parallel tasks - https://code.insistime.com/#/qiao-parallel