TypeScript 并发与事件模型

JavaScript/TypeScript 的并发模型以单线程事件循环(Event Loop)为核心,通过异步非阻塞 I/O 实现高并发。

跟 Python 比,TS 的并发模型更简单——只有一条主线:事件循环。没有 GIL,没有 asyncio.run() 和普通 def 的割裂,几乎所有 I/O 都天然返回 Promise。在 Python 里”并发模型选择题”(asyncio? 多线程?multiprocessing?)在 TS 里是内建答案。


事件循环:单线程的秘密武器

为什么单线程能高并发

Node.js/浏览器中的 JavaScript 是单线程的——同一时刻只执行一段代码。但这不代表它只能处理一个请求。秘密在于非阻塞 I/O + 事件驱动

你的 JS 代码(单线程)
   │
   ├── 发起 I/O 请求(文件读/网络/数据库)
   │        │
   │        └──→ 交给 libuv(C++ 线程池)处理
   │                  │
   │                  └──→ I/O 完成 → 回调放入任务队列
   │                                         │
   ├── 继续处理其他请求                       │
   │                                         │
   └── 事件循环检查任务队列 ←─────────────────┘
        有任务? → 取出执行 → 回到循环

关键:JS 代码本身是单线程的,但 I/O 操作由底层 libuv(C++ 线程池)异步执行。CPU 密集计算才真正阻塞事件循环。

事件循环的六个阶段(Node.js)

graph LR
    TIMERS["① timers<br/>setTimeout/setInterval"] --> PENDING["② pending callbacks<br/>系统操作回调"]
    PENDING --> IDLE["③ idle/prepare<br/>内部使用"]
    IDLE --> POLL["④ poll<br/>I/O 回调"]
    POLL --> CHECK["⑤ check<br/>setImmediate"]
    CHECK --> CLOSE["⑥ close callbacks<br/>close 事件"]
    CLOSE --> TIMERS

每个阶段执行完自己的回调队列后,检查是否有 process.nextTick 和微任务需处理,然后进入下一阶段。


宏任务与微任务:执行顺序的决定因素

两类任务

任务类型代表执行时机
宏任务 (MacroTask)setTimeoutsetIntervalsetImmediate、I/O每个事件循环阶段执行一个
微任务 (MicroTask)Promise.then/catch/finallyprocess.nextTick每个宏任务执行完后立即清空微任务队列

经典面试题(也是真实踩坑题)

console.log("1");
 
setTimeout(() => console.log("2"), 0);
 
Promise.resolve().then(() => console.log("3"));
 
process.nextTick(() => console.log("4"));  // Node.js 专有
 
console.log("5");
 
// 输出顺序:1 → 5 → 4 → 3 → 2
//
// 解析:
// 1. 同步代码先执行:1, 5
// 2. 微任务优先于宏任务:nextTick(4) > Promise(3)
// 3. 最后宏任务:setTimeout(2)

Python 对照:TS 的微任务 ≈ Python asyncio 中 await 后的协程恢复。区别在于 Python 的 asyncio 需要显式 awaitasync def,TS 的 Promise 链可以”隐性”创建微任务——.then() 调用即使不 await 也会排队执行。


Promise 与 async/await

Promise:异步操作的容器

// Promise 的三个状态:pending → fulfilled / rejected
const promise = new Promise<string>((resolve, reject) => {
  setTimeout(() => {
    if (Math.random() > 0.5) {
      resolve("成功");
    } else {
      reject(new Error("失败"));
    }
  }, 1000);
});
 
promise
  .then((data) => console.log(data))
  .catch((err) => console.error(err.message))
  .finally(() => console.log("清理"));

async/await:同步风格的异步代码

// async 函数返回 Promise
async function fetchUser(id: string): Promise<User> {
  const response = await fetch(`/api/users/${id}`);
  if (!response.ok) {
    throw new Error(`HTTP ${response.status}`);
  }
  return response.json(); // 类型:Promise<User>
}
 
// 错误处理:try/catch 自然捕获
async function main() {
  try {
    const user = await fetchUser("abc");
    console.log(user.name);
  } catch (err) {
    console.error("获取用户失败:", err);
  }
}

Promise 组合:并发控制

// 并行执行 — 全部成功才成功
const [user, posts, settings] = await Promise.all([
  fetchUser("abc"),
  fetchPosts("abc"),
  fetchSettings("abc"),
]);
 
// 竞速 — 最快返回的生效
const result = await Promise.race([
  fetch("/api/fast"),
  timeout(3000),
]);
 
// 全部完成(不管成败)— 查看每个的结果
const results = await Promise.allSettled([
  fetch("/api/may-fail-1"),
  fetch("/api/may-fail-2"),
]);
for (const r of results) {
  if (r.status === "fulfilled") {
    console.log(r.value);
  } else {
    console.error(r.reason);
  }
}

与 Python asyncio 的对照

维度TS Promise/asyncPython asyncio
异步关键字async function / awaitasync def / await
并发原语Promise.all / Promise.race / Promise.allSettledasyncio.gather / asyncio.wait
启动方式直接调用 async 函数需要事件循环 asyncio.run()
错误传播自动(Rejection 冒泡)自动(异常冒泡到 await 处)
任务取消AbortController(Web 标准)task.cancel()
生态系统几乎所有库都支持 Promise需要 async 版本(aiofiles, aiohttp)

经验迁移:如果你已经理解 Python 的 asyncio,TS 的 Promise/async-await 在心智模型上高度一致。最大区别是 TS 默认就是异步优先——几乎所有 I/O 操作都返回 Promise,你不需要像 Python 那样区分”同步版”和”异步版”的库。


Worker Threads:多线程的出口

为什么需要 Worker Threads

事件循环的软肋是 CPU 密集计算——一段耗时 5 秒的同步计算会阻塞整个事件循环,让所有并发请求排队等待。

// 这会阻塞事件循环 5 秒
function heavyComputation(n: number): number {
  let result = 0;
  for (let i = 0; i < n; i++) {
    result += Math.sqrt(i);
  }
  return result;
}
 
app.get("/compute", (req, res) => {
  const result = heavyComputation(1e8); // 阻塞!所有请求都在等
  res.json({ result });
});

Worker Threads 的基本用法

// main.ts — 主线程
import { Worker } from "worker_threads";
 
function computeInWorker(data: number): Promise<number> {
  return new Promise((resolve, reject) => {
    const worker = new Worker("./worker.js", {
      workerData: data,
    });
 
    worker.on("message", resolve);
    worker.on("error", reject);
    worker.on("exit", (code) => {
      if (code !== 0)
        reject(new Error(`Worker stopped with exit code ${code}`));
    });
  });
}
 
// 使用
const result = await computeInWorker(1e8);
// worker.ts — 工作线程
import { parentPort, workerData } from "worker_threads";
 
function compute(n: number): number {
  let result = 0;
  for (let i = 0; i < n; i++) {
    result += Math.sqrt(i);
  }
  return result;
}
 
const result = compute(workerData);
parentPort?.postMessage(result);

Worker Threads vs 子进程

维度Worker Threadschild_process
内存共享SharedArrayBuffer❌ 独立内存
通信方式postMessage(结构化克隆)IPC(串行化)
启动开销
隔离性低(共享进程)高(独立进程)
适用场景CPU 密集计算运行外部程序/高风险任务

与 Python 并发的对照

维度TSPython
默认模型单线程事件循环多线程(GIL)/ 多进程 / asyncio
CPU 密集解法Worker Threadsmultiprocessing / 3.14t 线程
I/O 密集解法事件循环(内置)asyncio(需要 async 生态)
共享内存SharedArrayBuffer(受限)multiprocessing.Value/Array
并发复杂度低(事件循环简化了大量问题)中(需选择 GIL 旁路方案)

TS 的并发模型比 Python 更”纯粹”——一个模型(事件循环)覆盖 90% 场景,Worker Threads 覆盖剩余 10%。Python 的 GIL/asyncio/multiprocessing 三套方案让开发者在并发问题上花更多时间做”选项分析”。这是 TS 作为”为 Web 而生”的语言的先天优势。


常见并发陷阱

1. Promise 忘记 await

// 不会等待,result 是 Promise<User>,不是 User
async function bad() {
  const result = fetchUser("abc"); // 类型:Promise<User>
  console.log(result.name); // 编译期可能不报错(取决于类型),运行时出错
}
 
// 必须 await
async function good() {
  const result = await fetchUser("abc");
  console.log(result.name);
}

2. 循环中的 async

// 串行执行 — 每次等上一个完成
for (const id of ids) {
  await processItem(id); // 总共耗时 = sum(每次耗时)
}
 
// 并行执行 — 全部同时发起
await Promise.all(ids.map(id => processItem(id)));
 
// 并发限流 — 最多 3 个同时执行
import pLimit from "p-limit";
const limit = pLimit(3);
await Promise.all(ids.map(id => limit(() => processItem(id))));

3. 未处理的 Promise Rejection

// 没有 .catch() 或 try/catch — 静默失败
fetchData().then(processResult);
 
// 始终处理错误
fetchData()
  .then(processResult)
  .catch(handleError);
 
// 或全局兜底
process.on("unhandledRejection", (reason) => {
  console.error("未处理的 Promise 拒绝:", reason);
});

关联