迁移基础 · 并发与可靠性 · LESSON 16

任务、线程与调度

区分并发、并行和异步,建立从事件循环到线程池的统一模型。

16 分钟tasks · threads · scheduling

任务、线程与调度

Node.js 的 event loop 适合等待 I/O,但 CPU 密集工作会阻塞循环。Python 和 C++ 提供线程、进程或任务库,但选择仍然取决于工作负载、数据大小、共享状态和截止时间。迁移 Promise 的经验时,要把“任务”理解成可调度单元:它有输入、开始时刻、资源、结果、取消方式和异常归属,而不是一个自动变快的关键字。

网络请求十个一组适合异步或线程池;图像预处理和模型推理可能需要进程、原生库或 GPU;实时控制通常需要固定周期线程和有界工作量。先测量每项工作是在等待、占 CPU,还是被锁和队列阻塞,再选择执行器。并发度过高会耗尽连接、内存或 GPU 显存,过低则浪费吞吐,不能靠直觉永久固定。

学习目标

  • 能依据 I/O 等待、CPU 计算、共享内存和取消需求选择协程、线程或进程。
  • 能限制并发数量并保持结果顺序,避免一次启动所有任务造成资源过载。
  • 能定义失败、超时和取消后的任务归属与清理方式。

先判断等待,还是计算

下面这段代码只保留同一个意图,重点观察输入边界、数据流和失败语义,而不是逐字符翻译。

TRANSLATION LENS 同一个意图,两种工程表达 窄屏可左右滑动查看完整代码
JS / TS
const results = await Promise.all(urls.map(fetchJson));
const summary = results.map(summarize);
Python / C++
# Python
async def load_all(urls):
  results = await asyncio.gather(*(fetch_json(url) for url in urls))
  return [summarize(item) for item in results]

// C++
std::vector<std::future<Item>> jobs;
for (const auto& url : urls) jobs.push_back(pool.submit(load, url));

从 Promise 到执行器

并发是同时推进,只有多核执行才一定涉及并行。Promise.allasyncio.gather 和 C++ future 都能表达等待多个结果,但对异常、取消和资源释放的默认行为不同。给任务编号并保留输入顺序,使用有界池限制同时运行的数量;机器人系统还要给任务设置 deadline,过期结果应丢弃或降级。

from concurrent.futures import ThreadPoolExecutor

def load_all(urls: list[str]) -> list[dict]:
    with ThreadPoolExecutor(max_workers=4) as pool:
        futures = [pool.submit(fetch_json, url) for url in urls]
        return [future.result() for future in futures]

选择 async、线程还是进程

async 适合大量等待且客户端支持异步;线程池适合阻塞 I/O 或能释放 GIL 的库调用;进程池适合隔离重计算,但序列化输入输出会有成本。C++ 线程池适合共享受控资源,任务队列应定义关闭和拒绝策略。Node 里可以用 worker thread 或子进程承载重计算,但不要把大对象无界复制给每个 worker。

常见错误与排错思路

常见错误是把十个 CPU 密集任务放进一个 event loop,或者创建任务后既不保存也不等待,导致异常无人读取。排错时记录 task_id、排队时间、运行时间、执行器和取消原因,观察队列长度与 CPU 使用率;若延迟抖动,比较并发度、锁等待和 GC/分配时间。先用一个任务复现,再逐步增加并发,能区分逻辑错误与资源饱和。

用有界 worker 控制并发量

Promise.all 很方便,但直接对大型输入映射会一次创建全部工作。worker pool 则由固定数量的执行者领取任务,保存输入索引以恢复原有顺序。下面是用于理解调度的最小示例;生产版本还需要取消信号、每项 deadline 与明确的失败策略。

async function mapLimit(items, limit, work) {
  const results = new Array(items.length);
  let next = 0;
  async function worker() {
    while (next < items.length) {
      const index = next++;
      results[index] = await work(items[index]);
    }
  }
  await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
  return results;
}
(async () => console.log(await mapLimit([1, 2, 3, 4], 2, async (n) => n * n)))();

预期结果为 [1, 4, 9, 16],同时活动 worker 不超过 2。这里的共享索引只适用于 JS 单线程事件循环中这段同步取号逻辑;移到多线程 C++ 后,分配任务必须用原子操作、互斥锁或线程安全队列。若任一任务失败,其他已启动 worker 不会因 Promise.all 自动停止,这一点必须在应用策略中显式处理。

运行验证:选择执行器并测量排队

网络 I/O 多且可取消时可考虑 async;阻塞 I/O 可考虑线程池;纯 CPU 工作可考虑进程池或 native 实现。评估还要计入序列化、线程切换、共享数据和启动成本。记录排队时间、运行时间、并发数和取消原因,避免只看总耗时。

验证时用可控 fake 将并发上限设为 1、2、4,检查最大活动数和输入/输出顺序;再让一项失败,观察取消和清理。若任务总是排队,增加线程可能只是耗尽资源,应先检查执行器容量与下游服务限制。

迁移练习

请完成:给一个同时请求十个服务的任务选择 async、线程池或进程池,并说明理由;再规定最多并发数、单项超时、取消动作和一个任务失败后的行为。

01
TRY IT YOURSELF

任务、线程与调度练习

给一个同时请求十个服务的任务选择 async、线程池或进程池,并说明理由;再规定最多并发数、单项超时、取消动作和一个任务失败后的行为。

给我一点提示

先判断请求是否主要等待网络,是否需要共享内存,再写出队列和异常处理策略。

查看参考答案
网络 I/O 优先 async 并限制 4 个并发;独立 CPU 工作可用进程池但需注意序列化;需要共享 C++ 状态时使用受控线程池和明确同步。每项设置 deadline,失败时取消未开始任务并等待已开始任务清理。
本节结论

统一理解任务调度,才能把 Promise 经验迁移到 asyncio 和机器人回调线程。完成后,请用计时、队列长度和失败计数验证你的执行器选择。

小结

任务是可调度、可取消并有资源成本的单元。先区分等待与计算,再设置并发上限、deadline 和失败策略;不要把 Promise 或 coroutine 直接等同于并行。

当前学习阶段并发与可靠性
0/8

阶段共 8 节课,按顺序完成更容易建立完整的迁移模型。