下面对比 Python asyncioawait 和 Node.js 中 await(基于 Promise)的异同,帮助你建立更精确的心智模型。

总览一句话

两边语法都写成 await expr,但:

  • Node.js await 等的是一个 Promise(或能被转成 Promise 的 thenable)。
  • Python await 等的是一个“可等待对象”(awaitable),核心是协程对象/Task/Future;调度由 asyncio 事件循环驱动。 语法相似,底层抽象与生命周期管理却不完全一样。

核心差异速览

维度Python asyncioNode.js
await 可等待的东西协程对象、asyncio.TaskFuture、实现 __await__ 的自定义对象Promise 或 thenable
启动方式创建协程对象后“惰性”不执行,直到第一次被 await/封装进 Task调用 async function 立即返回已开始执行的 Promise
并发单元协程 + Task(包装可独立调度)Promise(执行体在函数调用时已开始)
并发组合asyncio.gather, TaskGroup, waitPromise.all, Promise.race, allSettled, any
取消task.cancel() 发送 CancelledError 进协程;需要协程在可中断点让出promiseAbortController / 人为包装;Promise 本身没有原生取消(AbortController/第三方模式)
调度粒度基于显式 await/loop.call_soon;没有“微任务队列”概念libuv + 宏任务阶段 + 微任务(Promise jobs、process.nextTick()
异常传播协程里抛出异常,await 时重新抛出;未消费 Task 异常可触发日志Promise reject,在 await 处抛出;未处理的 rejection 触发 unhandledRejection 事件
重复等待同一个协程对象不能被多次并行 await;需先封成 Task同一个 Promise 可以被多次 await/then
创建时机差异协程体尚未执行(lazy),直到调度async 函数一调用立即执行到第一个 await
顶层 await交互式/3.11+ REPL 或特殊模式;脚本里用 asyncio.run() 包装ESM 模块原生支持顶层 await
自定义 await 行为可实现 __await__ 协议通过 thenable(实现 then 方法)
优雅关闭事件循环可收尾所有 Task (TaskGroup)进程退出前 pending Promise 需手动管理
并发隔离TaskGroup 提供结构化并发,异常会“汇聚”原生缺少结构化并发(可用库 “promise-pool” 等)
性能调度侧重点Python 侧等待点少、上下文切换开销更低于线程JS 事件循环内置 I/O 模型,配合微任务保证立即清算 Promise 回调

1. “启动时机” 是最容易混淆的点

Python:

async def coro():
    print("step1")
    await asyncio.sleep(0)
    print("step2")
 
c = coro()      # 此时不会打印,函数体尚未开始
await c         # 这里才开始执行

Node.js:

async function fn() {
  console.log('step1');
  await Promise.resolve();
  console.log('step2');
}
 
const p = fn(); // 这里立即打印 step1;开始执行到第一个 await
await p;        // 只是等待其完成

2. 多次等待行为差异

Python 不允许并发“二次 await”同一个协程对象(因为协程是一次性状态机):

c = coro()
await c
await c   # 第二次会抛 RuntimeError: cannot reuse already awaited coroutine

要多处等待,用 task = asyncio.create_task(coro()),然后多个地方 await 同一个 Task(结果共享)。

Node.js:

const p = fn();
await p;
await p; // OK:Promise 可多次等待;状态已缓存

3. 并发模式对比

Python(结构化并发):

async def a(): ...
async def b(): ...
results = await asyncio.gather(a(), b(), return_exceptions=False)

Node.js:

const [ra, rb] = await Promise.all([a(), b()]);

差异点:Python 3.11+ TaskGroup 会在一个任务失败后取消其它未完成任务,更偏向结构化并发安全;Node.js Promise.all 在其中一个 reject 时立即 reject,其它仍继续执行(无法自动取消,除非手动接入 AbortController/自定义逻辑)。

4. 取消机制

Python:

task = asyncio.create_task(long_job())
task.cancel()
try:
    await task
except asyncio.CancelledError:
    pass

Node.js(没有内建取消)常见写法:

const ac = new AbortController();
longJob({ signal: ac.signal });
ac.abort(); // 由 longJob 内部监听 signal 退出

或使用第三方库(e.g. p-cancelable),本质是模式约定。

5. 微任务 / job 阶段差异

Node.js 有“微任务队列”(Promise 回调、queueMicrotaskprocess.nextTick),保证在一次事件循环 tick 内尽快清空。 Python asyncio 没这个显式概念:协程切换只发生在:

  • await 一个真正挂起(Future 未完成)的对象
  • 显式 await asyncio.sleep(0)/await asyncio.yield_now()(3.11+)
  • 循环调度下一批 ready 回调

因此 Node.js 在 .then() 链里的 “同步 then” 更即时;Python 中“回到调度点”需要一次 loop 迭代。

6. 自定义可等待 vs thenable

Python:

class MyAwaitable:
    def __await__(self):
        yield from asyncio.sleep(1).__await__()
        return 42
 
result = await MyAwaitable()

Node.js:

const thenable = {
  then(resolve) {
    setTimeout(() => resolve(42), 1000);
  }
};
const v = await thenable;

两者都能被 await;协议不同:Python 用 __await__ 返回迭代器;Node 用 then 接口。

7. 错误传播与未处理错误

Python:Task 未被 awaited 且抛异常,会出现 “Task exception was never retrieved” 日志。 Node.js:Promise 未设置 .catch() 会触发 unhandledRejection(未来可能被视为致命错误,配置可变)。

8. 顶层 await

Node.js(ESM):

// 在模块最顶层
const data = await fetch(url);

Python 普通脚本不支持直接写顶层 await,常用:

import asyncio
asyncio.run(main())

交互模式(REPL / IPython / 3.12+ 某些上下文)可以顶层 await

9. 性能 & 设计哲学

  • Node.js 从底层 I/O(libuv)到语言原生 Promise,成体系;I/O 几乎都 Promise 化。
  • Python 原生库里很多 I/O 仍是阻塞式;asyncio + 第三方(aiohttp、asyncpg、Bleak)补齐生态;同步/异步世界并存,需要注意不要在协程里做长时间阻塞 CPU 的操作(否则要 run_in_executor)。

10. 在你的 BLE 示例中的实际意义

需求Python asyncio 做法Node.js 类比
扫描 + 并行等待超时asyncio.wait_for(scanner(), timeout=...)Promise.race([scan(), timeoutPromise])
注册通知回调await client.start_notify(...) + handler事件监听 characteristic.on('data', cb) / Promise 包装
多任务(心跳、监听、命令)TaskGroup / gather多个 async 函数 + Promise.all,但取消需手动广播

11. 容易踩的坑对照

场景Python 注意Node 注意
忘了 await协程对象未执行,静默Promise 已启动但未处理异常
重复 await协程报错OK
阻塞 CPU阻塞整个 loop(需 executor)同理阻塞事件循环
取消清理用 try/finally 释放资源监听 abort / finally 关闭句柄
竞争条件需要 asyncio.LockMutex/自写锁/原子引用

12. 心智模型对齐

Python: 协程 = 暂停/恢复的可迭代状态机;await = 把当前协程挂起,把控制权交还事件循环;事件完成后恢复。

Node.js: Promise = 最终值或错误的占位符;await = 暂停当前 async 函数,安排后续逻辑作为微任务回调;Promise settled 后恢复。

13. 一个“同构”示例对照

Python:

async def fetch_resolution(client):
    data = await client.read_gatt_char(CHAR_UUID)
    return parse(data)
 
async def main():
    # 并发两个操作
    r1, r2 = await asyncio.gather(fetch_resolution(c1), fetch_resolution(c2))

Node.js:

async function fetchResolution(client) {
  const data = await client.readChar(CHAR_UUID);
  return parse(data);
}
 
async function main() {
  const [r1, r2] = await Promise.all([
    fetchResolution(c1),
    fetchResolution(c2)
  ]);
}

表面极其相似,内部执行起点 & 取消控制不同。

14. 什么时候不必深究差异?

如果你只是“写一个顺序 await 流程”,两边思维几乎一致。 当你开始需要:

  • 可控取消
  • 结构化并发(异常传播策略)
  • 自定义 awaitable/thenable
  • 调度公平性 / yield 点 这些差异就变得重要。

15. 总结要点(记忆版)

  • Python 协程是 lazy,Node async 调用即刻执行。
  • Python 不能二次并发 await 同一协程;Node Promise 可以多次等待。
  • Python 有结构化并发(TaskGroup);Node 需要手动模式。
  • 取消:Python 原生,Node 需 AbortController 模式。
  • 微任务:Node 有明显“Promise job”阶段;Python 没同级概念。
  • 自定义:Python __await__,Node thenable。