取消术语
摘要
本文介绍了并发编程中同步取消、异步取消和优雅关闭的区别,并强调了它们在减少软件开发混淆方面的重要性。
<p><a href="https://lobste.rs/s/49hbhi/cancelation_terminology">评论</a></p>
查看缓存全文
缓存时间: 2026/09/01 11:46
# 取消操作的术语说明
来源:https://matklad.github.io/2026/08/31/cancelation-terminology.html
2026年8月31日
这篇短文解释了同步取消、异步取消和优雅停机之间的区别。我并不特别拘泥于这三个特定术语,但想请大家关注它们背后的三个概念——将这些概念相互混淆是非常危险的。
**同步取消**是一种(通常是隐式的)控制流结构。它会展开调用栈,代码如下所示:
```
task.cancel();
// 任务此时应该已完成
```
同步取消有点像莫里哀笔下的散文——我们一直在做,但未必完全意识到其存在。同步取消的主要来源是错误处理:每当抛出`Exception`或返回`error`时,代码会立即跳出所有循环、条件分支和代码块,通过RAII、`finally`、`with`/资源自动关闭或`defer`来执行必要的清理操作。
**异步取消**是两个实体之间的通信协议。一方请求取消(同步发起),但随后必须等待另一方确认并完成降级处理。代码如下:
```
task.request_cancelation();
// 任务此时可能仍在运行
task.join().await;
// 经过必要等待后,任务完成
```
与同步取消类似,这是在实现并发程序时确保不崩溃或不挂起的相对底层考量。我了解到两个需要异步取消的核心示例。
首先是CPU线程池。假设你将加密缓冲区的任务卸载到单独线程以处理用户请求。一段时间后,你得知该请求必须取消(可能用户已离开)。你不能直接放弃加密线程。首先,避免为无用工作浪费CPU周期是明智的,但更重要的是底层*缓冲区*必须保持绑定状态。如果因请求取消而释放该缓冲区,其他代码可能重用该内存,导致数据竞争。
但你也不能同步取消该线程!它正处于超优化的SIMD循环中,你确实不希望它在读取每个字节前都检查取消标志。你希望将缓冲区分割成合理大小的块,并在处理每个块后检查取消状态。但这意味着请求取消的一方必须等待至少一个块的处理时间!
另一个例子是`io_uring`。它具有完全相同的模式:如果你向内核提交带缓冲区的写操作,该缓冲区必须保持绑定直到写操作完成(你可以取消写操作使其更快完成)。虽然`io_uring`目前仍属于相对特殊的技术(尽管可以说,我们之前接触的才是更复杂的接口),但线程池示例表明异步取消现象本身是相当常见的。
异步取消在编写并发软件时经常出现。由于它影响代码的整体结构,尽早识别它非常有用。反之,自问是否真正需要异步取消,以及同步取消是否可行也同样重要。这在Rust中尤其重要——该语言使同步取消过于容易,却没有提供很好的异步取消机制。
最后,**优雅停机**是处理连接的应用程序设计模式。它位于比前两种取消更高的抽象层次。如果你正在实现Web服务,可以通过停止`accept`循环(拒绝新连接)来实现停机,同时继续服务所有现有连接直至客户端断开。如果负载均衡器配置为将新连接请求路由到服务的不同实例,此模式可实现滚动升级而不中断服务。
作为附加说明,相关概念是**仅崩溃软件**的理念。取消固然好,但你的整个程序可能因OOM守护进程而被任意SIGKILL,整台计算机可能因断电而重启。可靠的软件必须处理非优雅停机而不丢失数据。但是,如果你能承受断电情况,不妨通过SIGKILL自身来实现“退出”按钮,这样既简化了实现,又增加了对断电场景的测试覆盖。
---
以TigerBeetle为例,`Grid.cancel` (https://github.com/tigerbeetle/tigerbeetle/blob/47aeb2212a255273dda508288412e537d11e4b7c/src/vsr/grid.zig#L589)是异步取消。它接受一个回调函数来通知调用方取消完成。该API用于状态同步。当副本发现集群超前太多,基于事件的传输无法工作,需要状态传输来同步时,它必须取消所有未完成的网格读操作。读操作可能由副本本地磁盘支持,也可能从相邻副本透明获取数据。第一种情况我们需要等待读操作完成;第二种情况需要放弃读操作——远程读操作卡住很可能正是我们需要状态同步的原因。
`StateMachine.reset` (https://github.com/tigerbeetle/tigerbeetle/blob/47aeb2212a255273dda508288412e537d11e4b7c/src/state_machine.zig#L942)是同步取消的示例。这是与`Grid.cancel`相同流程的一部分,展示了如何通过清晰思考异步与同步取消来简化代码。虽然`StateMachine`位于`Grid`之上,中间还有若干层(`Forest`、`Tree`、`Compaction`、`Scan`等),但简单做法可能是发现`Grid`需要异步取消后就将异步性传播到整个技术栈。我们实际采用的方法是*仅*直接异步取消`Grid`,然后同步重置所有其他部分。
另一个异步取消的例子是`Client.shutdown` (https://github.com/tigerbeetle/tigerbeetle/blob/47aeb2212a255273dda508288412e537d11e4b7c/src/vsr/client.zig#L194-L203)。当使用TigerBeetle的应用程序“丢弃”`Client`对象时,我们需要释放所有操作系统资源。我们的客户端也使用io_uring,因此必须先等待所有未完成的系统调用完成。注释中我们称其为“优雅停机”,但我认为这不正确,这也是撰写本文的动机。TigerBeetle并不做优雅停机——我们始终采用仅崩溃模式。尾部延迟容忍(向多个节点请求答案并选择最快响应)是更通用的解决方案,它不仅处理崩溃故障,还能处理**灰色故障**。在分布式系统中,响应极慢的节点看起来与崩溃节点完全相同。崩溃只是迟缓的一种表现形式。
---
要点总结:
- 同步取消是控制流操作符
- 异步取消是通信协议
- 优雅停机是应用层设计模式
相似文章
Windows Runtime 活动的取消是异步的
本文通过代码示例解释了为什么 Windows Runtime 异步活动的取消是异步的,以及它如何避免死锁,尤其是在进度回调触发取消时。
Async/Await 的设计空间探索
本文介绍了编程语言中直线异步性的设计空间探索,审视了不同语言中async/await实现的差异及其语义后果。
异步编程的承诺与现实
深入剖析异步编程模型的演进——从回调到 Promise——揭示每一轮迭代如何解决先前的资源与性能问题,同时带来新的易用性挑战。
Tokio/Rayon 陷阱:为何 async/await 在并发中失灵
本文探讨了 async/await 语法虽然易于编写,但在生产环境中因将异步与并发混为一谈而引入了巨大复杂性,常需手动将 I/O 与计算任务分配到 Tokio 和 Rayon 等不同运行时,导致延迟飙升与系统不稳定。
并发、交互、可变:三者选二
文章探讨了编程语言中并发、交互性和可变性之间的固有权衡,通过Common Lisp、Python、Ruby和Erlang的例子说明没有语言能完全优化这三者。