← 中文资源

简体中文版本

取消令牌与执行预算

为每个工作任务共享子取消令牌,预算耗尽时统一触发取消,让所有协程在安全点退出。

阅读英文原文

功能说明

超时后直接丢弃任务 Future 虽然能停止等待,但任务内部的清理逻辑不会执行。取消令牌提供了协作式退出:一个父令牌派生多个子令牌,定时器到期时取消父令牌,所有监听子令牌的任务都会在 select 中走取消分支,完成日志、回滚和资源释放后再返回。

示例启动四个耗时递增的任务,预算为一百毫秒,前两个正常完成,后两个收到取消信号,最后统计成功返回的任务数量。所有任务都能优雅结束,没有强制丢弃。

关键代码

CancellationToken::new 创建父令牌,child_token 为每个任务派生独立令牌,取消父令牌会传播到所有子令牌。worker 用 tokio::select 同时等待取消信号与自身工作,取消分支优先结束。run_with_budget 另外启动一个定时任务,睡眠预算时间后调用 cancel。收集句柄时统计 Ok 的数量,取消任务同样返回 Ok,因为它只是走了另一个分支。

使用注意

取消是协作式的,任务必须在 await 点检查令牌,阻塞计算无法被打断。子令牌被取消后不会影响父令牌,适合单个任务提前退出。定时器任务应在主流程结束后等待完成,避免悬挂。预算应略大于正常最坏耗时,否则会误取消。任务返回 Ok 只表示协程正常结束,不代表业务成功,若需区分应在结果类型中标记取消状态。

验证方式

运行后打印前两个任务 finished,后两个 cancelled,最后输出 workers returned 为四,说明四个协程都正常收尾。把预算改成五十毫秒,只有第一个任务完成,其余三个被取消;把预算改成一秒,四个任务全部完成。可以给任务增加清理打印,确认取消分支也执行了释放逻辑。

代码示例

use std::time::Duration;
use tokio::time::sleep;
use tokio_util::sync::CancellationToken;

async fn worker(id: u32, token: CancellationToken) {
    tokio::select! {
        _ = token.cancelled() => {
            println!("worker {id} cancelled");
        }
        _ = sleep(Duration::from_millis(40 * id as u64)) => {
            println!("worker {id} finished");
        }
    }
}

async fn run_with_budget(budget: Duration) -> u32 {
    let token = CancellationToken::new();
    let mut handles = Vec::new();
    for id in 1..=4 {
        let child = token.child_token();
        handles.push(tokio::spawn(worker(id, child)));
    }

    let timer = {
        let token = token.clone();
        tokio::spawn(async move {
            sleep(budget).await;
            token.cancel();
        })
    };

    let mut returned = 0;
    for handle in handles {
        if handle.await.is_ok() {
            returned += 1;
        }
    }
    let _ = timer.await;
    returned
}

#[tokio::main]
async fn main() {
    let returned = run_with_budget(Duration::from_millis(100)).await;
    println!("workers returned: {returned}");
}