AI Library · 知识库

# 超时与取消:请求停止之后还要等待结束

学习目标:把等待超时、发出取消请求、任务实际结束三件事分开,并保证清理完成后调用方才继续。

前置知识:异步任务、错误分支、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,这是为了避免普通错误恢复意外屏蔽取消。

# 迁移误区与适用限制

# 运行与验证

两个完整程序预期输出:

纯文本completed
cancelled
timed out

断言检查成功提交、启动后取消、零时限、清理完成和失败路径不提交;没有固定耗时断言。没有网络、临时文件或未等待的线程。版本引用示例基线,实际运行记录见验证记录。

# 相关阅读