# 超时与取消:请求停止之后还要等待结束
学习目标:把等待超时、发出取消请求、任务实际结束三件事分开,并保证清理完成后调用方才继续。
前置知识:异步任务、错误分支、finally 或 defer;可先读并发等待。选择 Go 与 Python,对照显式取消通道和协程取消异常。只用标准库,操作由本机未开放的事件门闩模拟,不依赖公网或外部进程。
# 问题与边界
一个任务等待上游允许后,才能提交“后续工作”。调用方可以正常释放、主动取消或设置超时。三条路径分别返回成功、取消、超时;取消和超时路径都必须满足:后续工作没有发生,清理已完成,没有存活的后台任务。
示例用零时限覆盖已经到期的边界,用手动取消覆盖“工作已经启动但还在等待”的边界;不依赖某个操作恰好在 10 毫秒内完成。实际服务可以传入正时限,但调度与清理可能使总耗时超过时限。这里不实现强制杀线程,也不撤销已经提交的外部副作用。
# 关键差异
| 维度 | Go | Python |
|---|---|---|
| 取消信号 | Context.Done 关闭 | Task.cancel 请求注入 CancelledError |
| 工作者责任 | select 或检查 ctx.Err | 在可取消的 await 处让出,清理后不吞取消 |
| 超时呈现 | context.DeadlineExceeded | timeout 上下文外的 TimeoutError |
| 等待清理 | 独立 done 通道 | await 被取消任务或退出 timeout 块 |
Go 不会因为调用 cancel() 就自动停止 goroutine;Python 也不能依靠取消打断长期不让出的同步计算。取消是合作协议,不是“另一个词拼写的强制终止”。
# 各语言实现
# Go
context 规定取消传播与截止时间错误;取消函数释放关联资源,但不等待工作结束,所以另设 done。
Gopackage main
import (
"context"
"errors"
"fmt"
)
func run(mode string) error {
var ctx context.Context
var cancel context.CancelFunc
if mode == "timeout" {
ctx, cancel = context.WithTimeout(context.Background(), 0)
} else {
ctx, cancel = context.WithCancel(context.Background())
}
defer cancel()
ready := make(chan struct{})
release := make(chan struct{})
done := make(chan struct{})
result := make(chan error, 1)
cleaned := false
committed := false
go func() {
defer close(done)
defer func() { cleaned = true }()
close(ready)
select {
case <-ctx.Done():
result <- ctx.Err()
case <-release:
// 释放与取消可能同时发生,提交前再次检查。
if err := ctx.Err(); err != nil {
result <- err
return
}
committed = true
result <- nil
}
}()
<-ready
switch mode {
case "success": close(release)
case "cancel": cancel()
}
<-done // 在此之后才能读取工作者修改的状态。
err := <-result
if !cleaned || committed != (mode == "success") {
panic("lifecycle violation")
}
return err
}
func main() {
if err := run("success"); err != nil { panic(err) }
fmt.Println("completed")
if err := run("cancel"); !errors.Is(err, context.Canceled) {
panic("expected cancellation")
}
fmt.Println("cancelled")
if err := run("timeout"); !errors.Is(err, context.DeadlineExceeded) {
panic("expected timeout")
}
fmt.Println("timed out")
}
done 关闭发生在清理之后,接收方再读取状态,不依赖睡眠“猜测已经清理”。同步依据见 Go 内存模型中的通道通信。提交前的 ctx.Err() 只增加一个合作检查点,不能与任意外部取消形成事务:检查结束后仍可能收到取消。本文的失败用例在释放门闩之前取消,所以能够保证没有提交;生产中的付款、写库等需要自身的幂等或事务协议。
# Python
asyncio.timeout 通过取消当前任务实现超时,并在上下文之外转为 TimeoutError;Task.cancel 是请求,而不是已经终止的证明。
Pythonimport asyncio
async def work(release, started, state):
try:
started.set()
await release.wait()
state["committed"] = True
return "completed"
finally:
state["cleaned"] = True
async def main():
release, started = asyncio.Event(), asyncio.Event()
state = {"cleaned": False, "committed": False}
release.set()
assert await work(release, started, state) == "completed"
assert state == {"cleaned": True, "committed": True}
print("completed")
release, started = asyncio.Event(), asyncio.Event()
state = {"cleaned": False, "committed": False}
task = asyncio.create_task(work(release, started, state))
await started.wait()
task.cancel()
try:
await task
except asyncio.CancelledError:
assert task.cancelled()
else:
raise AssertionError("cancellation was lost")
assert state == {"cleaned": True, "committed": False}
print("cancelled")
release, started = asyncio.Event(), asyncio.Event()
state = {"cleaned": False, "committed": False}
try:
async with asyncio.timeout(0):
await work(release, started, state)
except TimeoutError:
assert started.is_set()
else:
raise AssertionError("timeout was lost")
assert state == {"cleaned": True, "committed": False}
print("timed out")
if __name__ == "__main__":
asyncio.run(main())
零时限在下一次事件循环调度时取消,工作协程先进入门闩等待,再执行 finally。手动取消路径由调用方消费 CancelledError,工作者没有吞掉它。不要把 except Exception 当成捕获所有取消的方式;CancelledError 直接继承 BaseException,这是为了避免普通错误恢复意外屏蔽取消。
# 迁移误区与适用限制
- 只给“等待结果”套超时,却把实际线程或进程留在后台:用户看到超时,任务仍可能修改状态。必须知道底层操作是否支持取消,并等待资源归还。
- 取消后立即退出主函数:清理尚未发生。示例用
done和await task把生命周期纳入契约。 - 捕获取消后继续循环:上层认为应停止,工作者却继续工作,TaskGroup/timeout 等结构化机制也可能被破坏。需要恢复策略时应明确设计,而不是顺手吞异常。
- Go 的
select在多个就绪分支中不优先选取消;检查取消也不是事务。本例特意控制门闩状态,避免用不稳定的竞态断言教学。 asyncio.to_thread里运行的同步函数不会因等待者取消就自动停止;CPU 长计算需要分块检查取消标志,或采用可管理生命周期的进程方案。
# 运行与验证
两个完整程序预期输出:
纯文本completed
cancelled
timed out
断言检查成功提交、启动后取消、零时限、清理完成和失败路径不提交;没有固定耗时断言。没有网络、临时文件或未等待的线程。版本引用示例基线,实际运行记录见验证记录。