在异步编程中, 经常遇到的需要实现取消操作这种需求, 比如在实现异步下载一个很大的文件时, 在下载过程中取消下载, 比如在发起异步网络读取时, 迟迟收不到数据时取消.
从异步编程的角度来看, 实现取消应该注意哪些问题呢? 以下是我个人的通过 asio
中的 cancellation
实现, 并思考总结出来的一些看法, 也希望能和大家一起深入的探讨.
在讨论取消之前, 我们先讨论一些其它的知识, 熟悉异步编程的朋友们一般都很清楚, 异步编程的范式通常分为2种形式, 一种是 eager
模式(eager
模式又被 microcai
称之为重叠 io 模式), 另一中则是 lazy
模式.
这 2 种模式本质上并没有太大区别, 都是异步完成模型(asio
异步编程默认为 eager
模式, 但也可 lazy
模式, 参考 asio
的deferred
等概念), 其主要区别则是在异步操作发起之后, 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 有相关的支持才有可能, 比如 windows
的 GetAddrInfoEx
就提供了相应取消的机制.
同样, 即使 GetAddrInfoEx
支持取消, 通过阅读 msdn
文档可以得知, 如果调用 CancelIoEx
之类的取消操作不及时而并不能取成功消.
而 lazy
模式则会有些区别, 在 lazy
模式中, 用户只要赶在向 os 提交异步操作前, 则都可以执行取消, 一旦异步操作已经提交到了 os 底层, 则和 eager
模式面临的情况是一样的.
所以, 到此为止, 我们大致可以做出下面一些结论性的总结:
-
首先, 异步取消应被设计为可选的, 这是因为很多操作本质上是无法被取消的.
-
其次, 异步取消是允许失败的, 也就是说, 即使你对一个异步操作发起取消, 但最终并没有如愿以偿的取消.
-
最后,
lazy
编程模式因为它的滞后特性, 所以可以实现在还未向 os 提交异步操作之前进行取消, 无论 os 是否支持取消.
Comments
blog comments powered by Disqus