功能说明
超时后直接丢弃任务 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}");
}