AI Library · 知识库

# 并发等待:先启动,再收齐结果

学习目标:同时发起独立操作,按输入顺序收集结果,并在失败时仍等待所有已启动任务结束。

前置知识:函数、异常、数组;需要知道 await 会挂起当前异步任务,不等于阻塞整个线程。本文选择 Python 与 JavaScript 的事件循环,全部使用标准库,不引入 HTTP 客户端。服务是本机事件门闩模拟的异步 I/O,不是公网请求,也不测网络性能。

# 问题与边界

输入为有限的服务名称列表。本例 alpha、beta 成功,bad 表示可预期的服务失败;成功结果按输入顺序返回,不依赖完成顺序。空列表返回空列表。有任何失败时,整批不返回部分成功值,而是在全部任务结束后报告第一个输入位置上的错误。

门闩先关闭,所有服务都被调度后才释放。这使等待真实交叠,又无需依赖毫秒数和调度顺序。结束计数在 finally 中更新,调用方断言返回或抛出前所有子任务已经清理。无限列表、请求超时和主动取消不在本篇契约内,见超时与取消。

# 关键差异

维度 Python JavaScript
开始执行 协程需包装成 Task 或交给 gather 调用 async 函数立即执行至首次挂起
等待全部结束 gather,return_exceptions=True Promise.allSettled
失败出口 收齐后重新抛出异常 收齐后抛出 rejected 的 reason
生命周期 保留任务并 await gather 保留 Promise 并 await allSettled

这里刻意选择“收齐后失败”。Python TaskGroup 的失败取消兄弟任务策略与此不同;JS Promise.all 的提前拒绝也不保证兄弟操作已经结束。机制不是同名方法之间的机械替换。

# 各语言实现

# Python

gather 保留输入顺序,return_exceptions=True 把异常放入结果集合;本例只在内部暂存异常,随后显式传播。

Pythonimport asyncio

async def fetch_all(names):
    gate = asyncio.Event()
    finished = []

    async def service(name):
        try:
            await gate.wait()
            if name == "bad":
                raise ValueError("service failed")
            return name.upper()
        finally:
            finished.append(name)

    tasks = [asyncio.create_task(service(name)) for name in names]
    # 让已创建的任务进入门闩等待,再统一释放。
    await asyncio.sleep(0)
    assert all(not task.done() for task in tasks)
    gate.set()
    results = await asyncio.gather(*tasks, return_exceptions=True)
    assert len(finished) == len(names)
    for result in results:
        if isinstance(result, BaseException):
            raise result
    return results

async def main():
    assert await fetch_all([]) == []
    result = await fetch_all(["alpha", "beta"])
    assert result == ["ALPHA", "BETA"]
    print(",".join(result))
    try:
        await fetch_all(["alpha", "bad"])
    except ValueError as error:
        assert str(error) == "service failed"
        print(error)
    else:
        raise AssertionError("failure was lost")

if __name__ == "__main__":
    asyncio.run(main())

asyncio.sleep(0) 是显式让出调度机会,不是模拟工作所需耗时。这里收集的异常只可能来自任务;业务成功值限定为字符串,避免把“异常对象也是合法数据”与失败混淆。若外层任务被取消,gather 会向未结束子任务传播取消;本篇驱动不触发这条路径。

# JavaScript

使用 CommonJS 以免依赖额外的模块配置。Node 提供 node:assert,Promise 的结果形状见 ECMAScript Promise.allSettled。

JavaScriptconst assert = require("node:assert/strict");

async function fetchAll(names) {
  let release;
  const gate = new Promise(resolve => { release = resolve; });
  let started = 0;
  let finished = 0;

  async function service(name) {
    started += 1;
    try {
      await gate;
      if (name === "bad") throw new Error("service failed");
      return name.toUpperCase();
    } finally {
      finished += 1;
    }
  }

  const tasks = names.map(service);
  assert.equal(started, names.length);
  assert.equal(finished, 0);
  release();
  const results = await Promise.allSettled(tasks);
  assert.equal(finished, names.length);
  const failure = results.find(result => result.status === "rejected");
  if (failure) throw failure.reason;
  return results.map(result => result.value);
}

async function main() {
  assert.deepEqual(await fetchAll([]), []);
  const result = await fetchAll(["alpha", "beta"]);
  assert.deepEqual(result, ["ALPHA", "BETA"]);
  console.log(result.join(","));
  await assert.rejects(fetchAll(["alpha", "bad"]), /service failed/);
  console.log("service failed");
}

main().catch(error => {
  console.error(error);
  process.exitCode = 1;
});

失败调用由 assert.rejects 显式消费并检查;非预期错误进入最外层处理器并设置非零退出状态。没有任何被遗忘的 Promise,也没有为“更快返回”而遗留后台请求。本篇没有重复 TypeScript 代码:比较的是 Python 与 JS 运行时调度语义,而非静态签名。

# 迁移误区与适用限制

# 运行与验证

两个程序的精确预期输出相同:

纯文本ALPHA,BETA
service failed

空输入、成功顺序、失败消费、全部任务结束由程序内断言覆盖,不使用计时断言。分别在独立目录运行两个文件。语言版本引用示例基线,实际运行状态以验证记录为准;本文给出的是预期行为,不代表所有基线工具链已经执行。

# 相关阅读