Jackarain 的 blog

喜欢 c++ 语言,在这里记录一些所见和所思...

论异步 cancellation

17 December 2024


在异步编程中, 经常遇到的需要实现取消操作这种需求, 比如在实现异步下载一个很大的文件时, 在下载过程中取消下载, 比如在发起异步网络读取时, 迟迟收不到数据时取消.

从异步编程的角度来看, 实现取消应该注意哪些问题呢? 以下是我个人的通过 asio 中的 cancellation 实现, 并思考总结出来的一些看法, 也希望能和大家一起深入的探讨.

在讨论取消之前, 我们先讨论一些其它的知识, 熟悉异步编程的朋友们一般都很清楚, 异步编程的范式通常分为2种形式, 一种是 eager 模式(eager 模式又被 microcai 称之为重叠 io 模式), 另一中则是 lazy 模式.

这 2 种模式本质上并没有太大区别, 都是异步完成模型(asio 异步编程默认为 eager 模式, 但也可 lazy 模式, 参考 asiodeferred 等概念), 其主要区别则是在异步操作发起之后, eager 模式则立即向 os 提交异步 io 请求, 接到请求的 os 内核将任务开始在内核中去执行, 与此同时, 用户代码也继续往下执行, 从逻辑上看起来就像是重叠(也可说是并行执行) 的一样.

lazy 编程模式则是创建了异步操作, 但并不立即向 os 提交异步请求, 而是直到用户代码调用等待其异步完成结果时, 才向 os 提交异步请求, (这也是 P2300 的 sender/recevier 异步模型默认采用的模式). 这个模式总结下来, 从整个 lazy 模式的运行过程, 它是将发起的异步请求操作放在最后需要的时候才提交到 os, 如此而已.

这 2 者哪个更优劣呢? 很显然, 从上面的描述来看, 我们可以看到 lazy 模式是有滞后性的问题, 而 eager 模式则会更及时的提交到 os, 从而获得更快的响应.

那么 lazy 模式是否一无是处呢? 并不是, lazy 模式通常用在 ”赛跑问题” 上, 就能发挥出它的优势了, 什么叫 ”赛跑问题” 呢? 让我举一个示例, 比如某业务需要同时发起多个异步数据库查询, 每个查询都是连接到了不同的数据库, 当第1个完成查询, 那么就可以取消其它所有查询操作, 示例代码如下所示:

// 各就各位
auto op1 = build_query();
auto op2 = build_query();
auto op3 = build_query();

[]

// 预备, 跑!
co_await (op1 || op2 || op3);

好了, 关于这 2 种模式的优劣讨论暂到此为止, 继续 cancellation 的话题, 首先讨论在 eager 模式上的取消, eager 模式因为它是立即提交异步操作的, 如果此异步操作并未提供相应的取消机制, 那么它就无法被取消, 甚至是即使提供了取消机制, 但是可能会因为调用取消的时机错失, 从而并不能成功的取消.

关于无法取消的操作, 比如发起 dns 请求调用 getaddrinfo 它是无法取消的, 一旦调用了 getaddrinfo 应用程序就只能乖乖等它返回而无法作任何取消, 如果需要支持取消, 只有 os 有相关的支持才有可能, 比如 windowsGetAddrInfoEx 就提供了相应取消的机制.

同样, 即使 GetAddrInfoEx 支持取消, 通过阅读 msdn 文档可以得知, 如果调用 CancelIoEx 之类的取消操作不及时而并不能取成功消.

lazy 模式则会有些区别, 在 lazy 模式中, 用户只要赶在向 os 提交异步操作前, 则都可以执行取消, 一旦异步操作已经提交到了 os 底层, 则和 eager 模式面临的情况是一样的.

所以, 到此为止, 我们大致可以做出下面一些结论性的总结:

  • 首先, 异步取消应被设计为可选的, 这是因为很多操作本质上是无法被取消的.

  • 其次, 异步取消是允许失败的, 也就是说, 即使你对一个异步操作发起取消, 但最终并没有如愿以偿的取消.

  • 最后, lazy 编程模式因为它的滞后特性, 所以可以实现在还未向 os 提交异步操作之前进行取消, 无论 os 是否支持取消.




Comments

blog comments powered by Disqus