一句话总结
Rust 异步的取消机制极简:drop 掉一个 Future,它就被取消了--停在最近的 .await 处,不再推进.
基于这一点,优雅关闭(graceful shutdown)的标准做法是:用一个信号通道(如 watch)广播"该收工了",各任务用 select! 监听它,收到就清理退出.
取消是怎么发生的
在很多语言里,"取消一个正在运行的任务"是个棘手问题--线程没法安全地从外部强杀.Rust 异步给出了一个出奇简单的答案:因为 Future 只在被 poll 时才推进,那么不再 poll 它(把它 drop 掉),它就自然停住了.
其实我们已经见过取消了.第 10 篇里:
select!选中一个分支后,其余分支的 Future 被 drop--那就是取消.timeout超时后,被包裹的 Future 被 drop--也是取消.
|
取消一个 Future 像合上一本正在读的书:不必把书"销毁",只要不再翻页(不再 poll),故事就停在当前页.
下次想继续得重新翻开--但被取消的任务通常不会被重新打开,它就此结束.
drop JoinHandle ≠ 取消任务
一个常见误解:以为丢弃 spawn 返回的 JoinHandle 就会取消任务.不会.
spawn 出去的任务由运行时独立持有,丢弃句柄只是不再关心它的结果,任务照常运行.要真正取消,需调用 handle.abort(),或用下面的信号机制让任务自己退出.
取消点在 await,不在中途
关键细节:取消只发生在 .await 处.一段连续的同步代码(两个 await 之间)一旦开始执行,不会被从中打断--它会跑完到下一个 await,才可能因不再被 poll 而停住.
这带来第 10 篇提过的取消安全问题:如果一个操作在某个 .await 处被取消,而它已经做了一半带副作用的工作(读走了一半数据,扣了款但没记账),就可能留下不一致状态.设计在 select! / timeout 中可能被取消的逻辑时,要确保"中途被取消也不破坏不变量".
优雅关闭:广播一个关闭信号
服务器收到 Ctrl-C 时,不该粗暴退出,而应通知所有任务"做完手头的事,清理,退出".标准模式是用一个关闭信号广播给所有任务.watch channel(第 09 篇)非常适合--它就是为"广播一个状态"设计的:
|
为什么用 select! 监听关闭
让每个任务在 select! 里同时等"正常工作"和"关闭信号".平时正常分支推进;一旦关闭信号到达,关闭分支胜出,任务跳出循环,执行清理.
这正是第 10 篇 select! "多路等待,谁先处理谁"能力的标准应用--把第 12 篇聊天室里那个 shutdown 分支泛化到所有任务.
CancellationToken:更顺手的取消工具
watch 能用,但当取消需要在多层任务树间传播时,tokio_util 提供的 CancellationToken 更专业:它可以克隆,可以派生子 token,父 token 取消时所有子 token 一并取消.
|
CancellationToken 像一个总闸:把它的副本发给所有相关任务,拉下总闸,所有任务同时收到"停"的信号.
比起手动用 watch 传 bool,它语义更专一,还支持父子级联,是生产级优雅关闭的常用选择.思路上很接近 Go 的 context.Context 取消传播.
和 Go context 的对照
Go 用 context.Context 贯穿调用链传播取消/超时,ctx.Done() 配合 select 监听.Rust 这边:CancellationToken ≈ context 的取消传播,tokio::time::timeout ≈ context.WithTimeout.
机制不同,但"用一个可传播的信号统一控制一批任务的生命周期"这个思想是相通的.
快速回顾
- 取消本质:drop 一个 Future 即取消,它停在最近的
.await处不再推进;select!/timeout 的"丢弃其余"就是取消. - drop 句柄 ≠ 取消:丢弃
JoinHandle不停止 spawn 的任务,需abort()或信号机制. - 取消点在 await:两个 await 之间的同步代码不被打断;注意取消安全,避免半完成的副作用.
- 优雅关闭:用
watch广播关闭信号 + 各任务select!监听 +ctrl_c()触发. - CancellationToken:可克隆,可级联的取消工具,适合多层任务树,思路类似 Go 的 context.
动手练习
- abort 取消:spawn 一个无限循环任务,用
handle.abort()取消它,验证它确实停止. - 验证 drop 句柄:验证"drop JoinHandle 不取消任务":丢弃句柄后,任务仍在打印,说明仍在运行.
- watch 优雅关闭:用
watch+select!+ctrl_c()实现:按下 Ctrl-C 后,三个工作任务都打印"退出"再结束. - CancellationToken:引入
tokio-util,用CancellationToken重写上一题. - 取消不安全实验:构造一个"取消不安全"的例子(如 select 里用 read_exact 读到一半被取消),思考它可能导致什么问题.