迁移基础 · 运行与表达 · LESSON 04

异步、并发与工程习惯

从 Promise 和 event loop 出发,建立对线程、任务和资源生命周期的直觉。

12 分钟async · concurrency · testing

async 不是“自动变快”

在 JS 中,async/await 让当前函数在 Promise 未完成时把控制权交回 event loop;它不会让同步计算突然获得第二个 CPU。Python 的 asyncio 有相同的协作式特征,协程只有在 await 处让出执行权;C++ 的 std::thread、任务库或执行器则可能真的并行执行,需要更明确地管理线程安全、取消和对象生命周期。

迁移前先画任务依赖:读取两个独立文件可以同时等待,依赖第一个结果的请求不能提前启动,汇总必须等两者完成。还要问资源何时释放、一个任务失败时其他任务是否取消、结果是否有顺序要求。AI 推理常是 CPU/GPU 密集,机器人回调还受截止时间约束,这些工程条件比关键字更重要。

TRANSLATION LENS 同一个意图,两种工程表达 窄屏可左右滑动查看完整代码
JS / TS
const response = await fetch(url);
const payload = await response.json();
Python
async with client.get(url) as response:
  payload = await response.json()

学习目标

  • 能区分并发、并行和异步等待,并根据任务瓶颈选择执行模型。
  • 能画出任务依赖、并发上限和失败传播方式,而不是看到 async 就并行化。
  • 能验证任务完成顺序、总耗时、取消清理与资源上限。

用工程问题而不是关键字做判断

如果工作主要等待网络或磁盘,异步能让一个线程服务更多连接;如果工作主要计算,必须考虑进程、线程池、原生库或 GPU。并发请求也要设置上限,否则 Promise.allgather 或无限任务提交会把连接池和内存耗尽。对机器人控制环,宁可丢弃过期传感器帧,也不要让旧任务排队到影响下一次控制。

const limit = 4;
const results: Result[] = [];
for (let i = 0; i < urls.length; i += limit) {
  const batch = urls.slice(i, i + limit);
  results.push(...await Promise.all(batch.map(fetchJson)));
}

错误传播、取消与清理

await 会把 Promise 的拒绝重新抛给调用方,但并不会自动取消已经发出的请求。Python 任务取消时要在 finally 关闭客户端,C++ 线程通常不能被强行杀死,应通过停止标志、队列关闭或 token 协作退出。测试时使用短超时和可控 fake,不要让网络偶发性决定测试是否通过。

做一个最小并发实验很有帮助:先用一个任务确认结果和错误,再把两个独立任务同时启动,最后逐步加入并发上限、超时和取消。每次只改变一个变量,记录总耗时、完成数和未完成数。这样可以把“并发代码偶尔出错”拆成可观察的调度、资源或清理问题。

常见错误与排错思路

最典型的问题是忘记 await,把 Promise/Task 当成数据继续传递;其次是在 async 函数里调用大段同步计算,导致 event loop 卡顿。排错时打印任务创建、开始、完成和取消时间,检查队列长度和每项耗时;若 C++ 出现偶发崩溃,先用线程 sanitizer 或缩小共享状态,再检查线程是否仍访问已销毁对象。

运行验证:一个可控的并发实验

先用一个固定的 fake 请求测出串行基线,再用两个、四个任务逐步增加并发。每次记录完成顺序、总耗时、最大同时任务数和取消后的清理结果。这个小实验能把“快了一点”变成可检查的证据,也能在进入线程池或机器人执行器前暴露任务依赖问题。

下面用一个不访问网络的假任务演示“等待可重叠,但结果仍可按输入顺序收集”。延迟仅用于展示调度顺序,实际性能判断必须用真实工作负载和重复测量。

const delay = (ms, value) => new Promise((resolve) => setTimeout(() => resolve(value), ms));
const started = Date.now();
const results = await Promise.all([delay(30, "first"), delay(10, "second")]);
console.log(results, Date.now() - started < 40);

预期数组仍是 first, second,而两个等待会重叠;计时断言只适合作为本地演示,不应用作 CI 性能门槛。Promise.all 在一个任务失败时会拒绝,但不会自动停止所有已开始任务,因此取消与资源清理必须由调用方另外设计。Python 的 asyncio.TaskGroup 和 C++ 任务库也需要显式决定失败后是取消同组工作还是收集部分结果。

结果检查与排错实验

先把任务数设为 1,再设为 2,最后设为 4;用固定延迟观察并发是否真的发生。随后人为让第二项失败,记录其他任务是否继续;再触发取消,确认文件句柄、连接和临时状态都能清理。若耗时没有改善,检查是不是同步 CPU 计算阻塞 event loop,而不是单纯增加任务数量。

任务依赖图与失败策略

把工作画成有向图:只有没有依赖边的任务才可同时启动。例如“读取配置 → 下载模型 → 推理 → 格式化输出”是串行链,而读取两个互不相关的文件可以并行;多个推理结果最终聚合时,需要规定是全部成功才返回,还是允许部分结果。这个决定会影响 Promise 聚合方式、Python task group 和 C++ future 的错误传播策略。

取消不是自动回滚

取消只表示调用方不再等待或希望任务停止,不代表已经发生的远程写入被撤销。为每项工作传入取消信号,并在等待点检查;执行文件写入、发送控制命令等副作用前再次确认 deadline。若取消发生在资源打开之后,仍需走清理路径。没有取消能力的底层任务应有有限超时,结果返回后也要避免继续修改已经失效的调用方状态。

运行验证:有界并发与失败

用 fake worker 记录开始/结束时的 active 数值,断言峰值不超过上限、所有输入都有明确结果或错误。再令第 2 项立即失败,确认后续任务是否还会被领取;最后触发取消并检查未完成任务有没有残留。结果顺序、最大并发、失败数量和清理完成时间都要成为测试观测项,不能只用“执行比串行快”作为通过标准。

任务失败后的系统行为

启动并发任务前先决定整体语义。对于搜索候选项,“找到一个就取消其余任务”可能合适;对于多个传感器通道汇总,可能需要等齐所有结果;对于独立的模型评估,则可能接受部分结果并附带失败清单。这个选择决定是否使用 fail-fast 聚合、逐项收集,还是分阶段提交,不能等到第一条异常出现时才临时决定。

有界队列与背压

并发上限控制同时运行数量,队列容量控制等待任务数量,两者不是同一限制。若生产者持续快于消费者,即使活跃任务数固定,等待队列仍可能无限增长。为队列设最大长度,并选择满载时阻塞、拒绝、丢弃最旧或合并重复任务;把该策略写入业务契约,记录 queue_depth、rejected_total 和等待时长。

运行验证:观察整个任务生命周期

给每项工作分配 task_id,记录 queued、started、completed、failed 或 cancelled 的转换。测试慢消费者、突发流量、首项失败和调用方取消,确认任务不会重复领取、失败不会无人读取、队列有上限且取消最终释放资源。若并发后只是完成时间变短,但失败计数和队列长度不可见,就还不能判断这个调度器是否稳定。

决定并发模型的工程检查表

工作负载候选模型迁移时特别验证
多个独立网络等待async / 协程并发上限、超时与取消
阻塞式文件或 SDK 调用线程池SDK 是否线程安全、线程数是否受控
大量纯 CPU 计算进程池或 native 实现数据复制、序列化和启动开销
共享硬件控制状态单一执行者或受控队列命令顺序、截止时间和安全停机

选择不是语言排行榜。用同一批输入测串行基线与目标模型的吞吐、p95 延迟、峰值内存和失败行为;若多进程复制大型张量的成本高于计算收益,增加 worker 反而会更慢。若 C++ 线程共享设备 SDK,先确认 SDK 的并发保证,不能从“程序编译通过”推断线程安全。

任务状态与取消传播

给任务定义 queued、running、completed、failed、cancelled 状态,并规定哪些状态可以转换。取消只能阻止尚未发生的工作,无法自动撤回已提交的远程请求或文件写入;对这类操作要使用幂等键、补偿动作或显式“结果未知”状态。父任务取消时应向子任务传播信号、等待资源清理,并决定部分结果是否可返回。

运行验证:调度压力与恢复

从 1 个 worker 开始,逐步增加到 2、4,观察活动任务数、队列长度和下游错误率;再用慢消费者制造背压,用一次任务失败检验其余任务策略,用取消检验清理时限。固定 fake 的耗时和失败位置,测试才能稳定重放。通过条件应同时包含数量正确、顺序符合契约、队列有界和取消后无资源泄漏。

迁移练习

01
TRY IT YOURSELF

找出可以并发的任务

实现一个最多同时请求 4 个 URL 的小函数,并为一个请求失败、调用方取消、结果仍需按输入顺序返回写出处理策略。再标出哪些步骤是等待,哪些步骤是计算。

给我一点提示

只有互相不依赖的工作才适合同时开始;限制并发数,并把取消和清理写进 finally/RAII。

查看参考答案
用批次或有界信号量限制 4 个任务;保留输入索引即可按原顺序收集。失败时取消尚未开始的任务并等待已开始任务清理资源,网络等待用 async,重计算另行放入线程或进程执行。
本节结论

从 JS/TS 迁移到 Python/C++,并发的难点不在 API 名字,而在显式处理任务依赖、错误传播、取消、背压和资源生命周期。

小结

异步主要改善等待期间的资源利用,线程或进程才可能把计算分配到其他执行单元。先测量任务类型,再规定上限、结果顺序和取消清理;接下来学习任务调度时,可把这些条件扩展到多个执行器。

当前学习阶段运行与表达
0/7

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