JavaScript 中的并发与多线程:如何让代码运行更快
JavaScript 历史上是一种单线程语言,但这并不意味着它不能同时处理多个任务。在现代 Node.js 和浏览器中,存在强大的机制来处理并发和多线程。理解这些概念对于创建高性能的服务器端和客户端应用程序至关重要。
在本文中,我们将分析并发与并行有何不同,Event Loop 如何工作,何时使用 Worker Threads 和 Web Workers,并探讨 Node.js 中的实际示例。
1. Event Loop 与非阻塞 I/O
JavaScript 中并发的基础是 Event Loop(事件循环)。这是一种机制,允许执行异步操作(例如,读取文件、数据库查询)而不阻塞主线程。
想象一个队列:Event Loop 从队列(call stack)中取出一个任务,执行它,如果遇到异步操作(例如,setTimeout 或 fetch),则将其传递给 Web API(在浏览器中)或 libuv(在 Node.js 中)。操作完成后,其回调函数进入任务队列(task queue)。Event Loop 不断检查调用栈是否为空,如果为空,则从队列中取出下一个任务。
异步代码示例:
const fs = require('fs');
console.log('开始');
fs.readFile('example.txt', 'utf8', (err, data) => { if (err) throw err; console.log('文件已读取:', data);});
console.log('结束');
// 输出:// 开始// 结束// 文件已读取: ...请注意:console.log('结束') 在文件读取之前执行。这就是非阻塞 I/O —— 主线程不会等待操作完成,而是继续执行。
2. Web Workers 和 Worker Threads:真正的多线程
虽然 Event Loop 能很好地处理 I/O 操作,但它无法帮助执行“繁重”的计算(图像处理、加密、复杂排序)。如果在主线程中运行此类代码,整个界面(或服务器)将“挂起”直到计算完成。
为了解决这个问题,存在 Web Workers(在浏览器中)和 Worker Threads(在 Node.js 中)。它们允许在单独的操作系统线程中运行 JavaScript 代码。
Node.js 中 Worker Threads 的示例
创建一个 worker.js 文件:
const { parentPort } = require('worker_threads');
// 繁重任务:计算数字总和let sum = 0;for (let i = 0; i < 1e9; i++) { sum += i;}
// 将结果发送回主线程parentPort.postMessage(sum);主文件 main.js:
const { Worker } = require('worker_threads');
console.log('主线程:启动 worker');
const worker = new Worker('./worker.js');
worker.on('message', (result) => { console.log('来自 worker 的结果:', result);});
worker.on('error', (err) => { console.error('Worker 错误:', err);});
worker.on('exit', (code) => { if (code !== 0) { console.error(`Worker 以代码 ${code} 退出`); }});
console.log('主线程:继续工作...');
// 输出:// 主线程:启动 worker// 主线程:继续工作...// 来自 worker 的结果: 499999999500000000如您所见,主线程不会被阻塞,而 worker 在后台执行计算。这就是真正的多线程。
3. Async/Await 和 Promise:管理并发
现代 JavaScript 提供了处理异步代码的优雅方式 —— async/await 和 Promise。它们不会创建新线程,但