并发与并行的区别

深入理解 JavaScript 的并发模型、与多线程并行的本质区别,以及 Web Worker 在实际项目中的应用场景。


概念辨析

并发(Concurrency):两个或多个任务交替执行,在一段时间内看起来像是同时进行。CPU 在不同任务之间快速切换。

并行(Parallelism):两个或多个任务真正同时执行。需要多个 CPU 核心。

JavaScript 是单线程的,但它通过事件循环实现了并发——不是并行。


JavaScript 的并发模型

JavaScript 的并发靠事件循环实现。同一时刻只有一个任务在执行,但任务之间交替执行,给人的感觉像是"同时"执行。

// 这个函数不会阻塞其他代码
function longComputation() {
  let result = 0;
  for (let i = 0; i < 1e9; i++) {
    result += i;
  }
  return result;
}

// 虽然 longComputation 需要很长时间,
// 但浏览器仍然可以处理用户点击、滚动等事件
// 因为事件循环会在 longComputation 的间隙中处理这些事件

为什么 JavaScript 选择单线程

JavaScript 最初是为浏览器设计的脚本语言。浏览器环境中,如果 JavaScript 多线程操作同一个 DOM,会产生复杂的同步问题。

单线程的优势:

  • 无竞态条件:不需要锁或同步机制
  • 简单:编程模型简单,不需要考虑线程安全
  • 适合 IO 密集型:前端主要是 IO 操作(网络请求、用户交互),CPU 计算不是瓶颈

CPU 密集型任务的挑战

单线程模型在 CPU 密集型任务面前显得无力:

// 这个计算会阻塞主线程 1 秒
function fibonacci(n) {
  if (n <= 1) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
}

console.log(fibonacci(45)); // 阻塞约 1 秒

Web Worker:真正的并行

Web Worker 是在后台线程运行 JavaScript 的机制,可以实现真正的并行:

// main.js
const worker = new Worker('worker.js');

worker.postMessage({ n: 45 });

worker.onmessage = function(e) {
  console.log('Result:', e.data);
};

// worker.js
self.onmessage = function(e) {
  const result = fibonacci(e.data.n);
  self.postMessage(result);
};

function fibonacci(n) {
  if (n <= 1) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
}

Worker 的限制

  • Worker 不能直接操作 DOM
  • Worker 与主线程通过消息传递通信
  • 数据是复制的,不是共享的

Service Worker:后台任务的另一种形态

Service Worker 是浏览器在后台运行的另一个脚本,可以:

  • 拦截网络请求(实现离线缓存)
  • 推送通知
  • 后台同步
navigator.serviceWorker.register('/sw.js');

navigator.serviceWorker.addEventListener('message', event => {
  console.log('Data from worker:', event.data);
});

实际应用场景

适合 Worker 的场景

  • 复杂的数学计算(加密、图像处理)
  • 大数据排序、搜索
  • 语法高亮、Markdown 解析等耗时操作

不需要 Worker 的场景

  • 简单的 DOM 操作
  • 小数据量处理
  • 网络请求

这一章想说的

JavaScript 通过单线程 + 事件循环实现了并发,但不是并行

对于 IO 密集型任务(前端的主要场景),并发模型足够。但对于 CPU 密集型任务,需要用 Web Worker 将计算移到后台线程,实现真正的并行。

使用 Worker 的关键是:不要共享状态,只传递数据。Worker 和主线程之间的通信是异步的,遵循消息传递模式。


延展阅读