任务、线程与调度
区分并发、并行和异步,建立从事件循环到线程池的统一模型。
任务、线程与调度
Node.js 的 event loop 适合等待 I/O,但 CPU 密集工作会阻塞循环。Python 和 C++ 提供线程、进程或任务库,但选择仍然取决于工作负载、数据大小、共享状态和截止时间。迁移 Promise 的经验时,要把“任务”理解成可调度单元:它有输入、开始时刻、资源、结果、取消方式和异常归属,而不是一个自动变快的关键字。
网络请求十个一组适合异步或线程池;图像预处理和模型推理可能需要进程、原生库或 GPU;实时控制通常需要固定周期线程和有界工作量。先测量每项工作是在等待、占 CPU,还是被锁和队列阻塞,再选择执行器。并发度过高会耗尽连接、内存或 GPU 显存,过低则浪费吞吐,不能靠直觉永久固定。
学习目标
- 能依据 I/O 等待、CPU 计算、共享内存和取消需求选择协程、线程或进程。
- 能限制并发数量并保持结果顺序,避免一次启动所有任务造成资源过载。
- 能定义失败、超时和取消后的任务归属与清理方式。
先判断等待,还是计算
下面这段代码只保留同一个意图,重点观察输入边界、数据流和失败语义,而不是逐字符翻译。
const results = await Promise.all(urls.map(fetchJson));
const summary = results.map(summarize); # 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.all、asyncio.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、线程池或进程池,并说明理由;再规定最多并发数、单项超时、取消动作和一个任务失败后的行为。
任务、线程与调度练习
给一个同时请求十个服务的任务选择 async、线程池或进程池,并说明理由;再规定最多并发数、单项超时、取消动作和一个任务失败后的行为。
给我一点提示
先判断请求是否主要等待网络,是否需要共享内存,再写出队列和异常处理策略。
查看参考答案
网络 I/O 优先 async 并限制 4 个并发;独立 CPU 工作可用进程池但需注意序列化;需要共享 C++ 状态时使用受控线程池和明确同步。每项设置 deadline,失败时取消未开始任务并等待已开始任务清理。 本节结论
统一理解任务调度,才能把 Promise 经验迁移到 asyncio 和机器人回调线程。完成后,请用计时、队列长度和失败计数验证你的执行器选择。
小结
任务是可调度、可取消并有资源成本的单元。先区分等待与计算,再设置并发上限、deadline 和失败策略;不要把 Promise 或 coroutine 直接等同于并行。
阶段共 8 节课,按顺序完成更容易建立完整的迁移模型。