本节目标:从 CPython 源码看清
Task/Future的状态机与cancel()的注入机制,掌握cancelling()、uncancel()、shield的精确语义。
适用版本:Python 3.12+(实测 3.14.6;cancelling()/uncancel()为 3.11+)
6.2 任务、Future 与取消语义
asyncio 完全指南
给过一张 Task vs 裸协程的对照表,高级异步一篇
则断言 asyncio「取消机制脆弱」。本节不重复这些结论,而是把 cancel() 拆到字节级:它到底改了什么字段、异常在哪一行被抛进协程、为什么 cancel() 返回后 task.done() 还是 False。
6.2.1 Task 就是 Future 的子类
很多人以为 Task 和 Future 是两种东西,其实 Task 只是「会自己推进的 Future」。在本机打印 MRO:
import asyncio
from asyncio import Task, Future
print("Task MRO:")
for c in Task.__mro__:
print(" ", c.__module__ + "." + c.__name__)
print("issubclass(Task, Future) =", issubclass(Task, Future))
实测输出:
Task MRO:
_asyncio.Task
_asyncio.Future
builtins.object
issubclass(Task, Future) = True
注意 Task 的实际类型是 _asyncio.Task(C 加速版),基类是 _asyncio.Future,两者都不在 asyncio 命名空间里,而是来自 C 扩展模块 _asyncio。纯 Python 版本在 asyncio.tasks.Task(本机 asyncio.Task is asyncio.tasks._PyTask 为 False,说明 C 版已启用)。
这个继承关系决定了三件事:
Task拥有Future的全部 API:result()、exception()、add_done_callback()、cancel()。Task可以被await,因为Future.__await__已经实现了「挂起直到完成」。Task比Future多出来的,只有「驱动协程」这一步——由__step完成。
6.2.2 Future 的三态机
Future 内部用一个字符串字段 _state 表示状态,只有三个取值:PENDING、FINISHED、CANCELLED。实测状态迁移:
import asyncio
async def main():
loop = asyncio.get_running_loop()
f = loop.create_future()
print("新建:", f._state)
f.set_result(42)
print("set_result 后:", f._state, "| result:", f.result())
try:
f.set_result(1) # 重复设置
except Exception as e:
print("重复 set_result ->", type(e).__name__, e)
f2 = loop.create_future()
f2.cancel()
print("cancel 后:", f2._state, "| cancelled():", f2.cancelled())
try:
f2.result()
except asyncio.CancelledError:
print("cancelled future 的 result() -> CancelledError")
asyncio.run(main())
实测输出:
新建: PENDING
set_result 后: FINISHED | result: 42
重复 set_result -> InvalidStateError invalid state
cancel 后: CANCELLED | cancelled(): True
cancelled future 的 result() -> CancelledError
三点结论:
| 操作 | _state 迁移 | 后续行为 |
|---|---|---|
set_result / set_exception | PENDING → FINISHED | 再次设置抛 InvalidStateError |
cancel() | PENDING → CANCELLED | result() 抛 CancelledError |
已完成的 Future 再 cancel() | 不变 | 返回 False |
状态是单向的:一旦离开 PENDING 就再也回不去。done() 等价于 _state != 'PENDING',所以 FINISHED 和 CANCELLED 都算「完成」。
6.2.3 cancel() 把异常注入到哪里
Task.cancel() 和 Future.cancel() 是两个不同的实现。Future.cancel() 只是把状态改成 CANCELLED;Task.cancel() 要复杂得多。以下是纯 Python 版 _PyTask.cancel 的核心(asyncio/tasks.py,与 _asynciomodule.c 逐行对应):
def cancel(self, msg=None):
self._log_traceback = False
if self.done():
return False
self._num_cancels_requested += 1 # ← cancelling() 计数器
if self._fut_waiter is not None:
if self._fut_waiter.cancel(msg=msg):
return True # 取消成功传递给内层 Future
self._must_cancel = True # ← 否则记一个「待注入」标志
self._cancel_message = msg
return True
关键在 _fut_waiter 和 _must_cancel 这两个字段:
_fut_waiter:当前协程正await的那个 Future。若存在,cancel()会顺着链条往下取消——递归地把取消请求传给内层。_must_cancel:若协程当前不在等 Future(比如刚被调度、还没跑到await),就设这个标志,等下一次__step时再注入。
而 __step 的入口负责真正把异常抛进协程:
def __step(self, exc=None):
if self.done():
raise InvalidStateError(...)
if self._must_cancel: # 上一轮设的待注入标志
if not isinstance(exc, CancelledError):
exc = self._make_cancelled_error() # 换成一个 CancelledError
self._must_cancel = False
self._fut_waiter = None
...
# 内部最终调用:
# result = coro.send(None) # 正常推进
# result = coro.throw(exc) # 把 CancelledError 抛进挂起点
所以「取消」的本质是:把一个 CancelledError 通过 coro.throw() 扔进协程上次 await 的位置。协程的 try / except / finally 都能拦到它——这正是「取消是协作式」的根源:任务可以选择捕获、清理,甚至拒绝取消。
6.2.4 实测:取消到底在哪一刻生效
cancel() 的注释里有一句关键话:「调用后立刻 cancelled() 不会返回 True」。实测时序:
import asyncio
T0 = None
def now():
return round(asyncio.get_running_loop().time() - T0, 4)
async def worker():
try:
print(f"{now()} worker: 进入 sleep(1)")
await asyncio.sleep(1) # ← 取消注入点
except asyncio.CancelledError:
print(f"{now()} worker: 捕获 CancelledError")
raise # 规范:继续向上传播
finally:
print(f"{now()} worker: finally 清理")
async def main():
global T0
T0 = asyncio.get_running_loop().time()
t = asyncio.create_task(worker())
await asyncio.sleep(0.1)
print(f"{now()} main: 调用 t.cancel()")
t.cancel()
print(f"{now()} main: cancel() 返回,此刻 t.done()={t.done()}")
try:
await t
except asyncio.CancelledError:
print(f"{now()} main: await t 收到 CancelledError")
print(f"{now()} main: 最终 _state={t._state} cancelled={t.cancelled()}")
asyncio.run(main())
实测输出:
0.0 worker: 进入 sleep(1)
0.1024 main: 调用 t.cancel()
0.1037 main: cancel() 返回,此刻 t.done()=False
0.1038 worker: 捕获 CancelledError
0.1038 worker: finally 清理
0.104 main: await t 收到 CancelledError
0.104 main: 最终 _state=CANCELLED cancelled=True
把时间轴拆开看:
cancel()在 0.1024 被调用,0.1037 就返回了——它不做任何等待,只是改了字段。- 返回瞬间
t.done()仍是False:异常还没注入。 - 0.1038,
worker在await asyncio.sleep(1)处被throw了CancelledError,立刻跳进except和finally。 - 0.104,
main的await t才收到CancelledError。
一句话:cancel() 是「请求」,异常注入发生在下一个事件循环迭代。这也是为什么 cancel() 之后必须 await 任务(或用 TaskGroup),否则清理逻辑可能还没跑完程序就退出了。
6.2.5 shield:保护边界在哪里
asyncio.shield() 用来「让外层取消不了内层」。实测:
import asyncio
async def inner():
try:
await asyncio.sleep(0.3)
print("inner: 完整跑完(被 shield 保护)")
return "ok"
except asyncio.CancelledError:
print("inner: 被取消(不该出现)")
raise
async def main_shield():
task = asyncio.create_task(inner())
try:
await asyncio.wait_for(asyncio.shield(task), timeout=0.1)
except TimeoutError:
print("外层: 超时,但 task 仍在跑,done()=", task.done())
res = await task # 外层超时不影响内层
print("外层: 之后拿到结果 =", res)
asyncio.run(main_shield())
实测输出:
外层: 超时,但 task 仍在跑,done()= False
inner: 完整跑完(被 shield 保护)
外层: 之后拿到结果 = ok
shield 的语义要精确理解:它不阻止取消,只是拦在外层和 task 之间。外层取消 shield 包装出来的那个 Future 时,task 本身不受影响;但 shield 的文档明确写着——外层被取消后,被保护的任务最终仍需由你负责,否则会变成孤儿任务。所以 shield 只适合「关键收尾不能被打断」,不适合当万能保险。
6.2.6 uncancel():撤销一次取消请求
Task.cancel() 每次调用都会把 _num_cancels_requested 加一。3.11 起新增两个配套 API:cancelling() 读这个计数,uncancel() 把它减一。实测:
import asyncio
async def child():
try:
await asyncio.sleep(10)
except asyncio.CancelledError:
print("child: cancelling() =", asyncio.current_task().cancelling())
asyncio.current_task().uncancel() # 撤销一次取消请求
print("child: uncancel 后 cancelling() =", asyncio.current_task().cancelling())
return "cleaned" # 正常返回,不重新抛出
async def main():
t = asyncio.create_task(child())
await asyncio.sleep(0.05)
t.cancel()
res = await t
print("main: child 正常返回 =", res, "| cancelled() =", t.cancelled())
asyncio.run(main())
实测输出:
child: cancelling() = 1
child: uncancel 后 cancelling() = 0
main: child 正常返回 = cleaned | cancelled() = False
这套计数器的价值在于区分「真取消」和「取消被吞掉」。TaskGroup、asyncio.timeout 等结构化 API 内部都会先看 cancelling() 计数:如果子任务捕获了 CancelledError 却没 uncancel(),框架就知道它在偷偷吞取消,会重新抛出。手写「清理后继续」的代码时,正确姿势就是先 uncancel() 再 return,而不是裸 except CancelledError: pass。
小结
Task是Future的子类(C 版为_asyncio.Task→_asyncio.Future),多出来的只有「驱动协程」的__step。Future状态是单向三态机:PENDING→FINISHED/CANCELLED,重复set_result抛InvalidStateError。Task.cancel()只是改字段:有_fut_waiter就向下传递取消,否则设_must_cancel;异常由下一次__step用coro.throw()注入。- 取消是协作式的:
cancel()返回后任务仍PENDING,异常在下一次循环迭代才到达挂起点。 shield只拦截外层取消,不负责被保护任务的收尾;uncancel()用于「清理后拒绝取消」,配合cancelling()计数区分真取消与吞取消。
理解了单任务的取消,下一节看多个任务怎么被组织成一个「结构化」的整体——TaskGroup、anyio 与异常组传播。
阅读导航:上一节:事件循环的实现与调度 · 下一节:结构化并发与调试 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。