Boost.Asio:如何取消Awaitable且不引发程序终止?
Boost Asio协程co_spawn取消导致程序终止问题的解决办法
问题描述
示例代码
#include <boost/asio/awaitable.hpp> #include <boost/asio/bind_cancellation_slot.hpp> #include <boost/asio/cancellation_signal.hpp> #include <boost/asio/co_spawn.hpp> #include <boost/asio/detached.hpp> #include <boost/asio/io_context.hpp> #include <boost/asio/use_awaitable.hpp> namespace ba = boost::asio; ba::cancellation_signal cancel_sub; void subscribe(ba::io_context &context) { ba::co_spawn( context, []() -> ba::awaitable<void> { co_return; }, ba::bind_cancellation_slot(cancel_sub.slot(), ba::detached)); cancel_sub.emit(ba::cancellation_type::all); } int main() { ba::io_context ctx; subscribe(ctx); ctx.run(); return 0; }
错误输出
程序运行后因内部抛出未捕获异常终止:
terminate called after throwing an instance of 'boost::wrapexcept<boost::system::system_error>' what(): co_await: Operation canceled [system:125]
尝试过的无效方案
- 在协程内禁用取消抛出异常:
co_await ba::this_coro::throw_if_cancelled(false); - 在协程开头重置取消状态:
co_await ba::this_coro::reset_cancellation_state(ba::enable_total_cancellation());
异常流程分析
通过Boost内部代码追踪,异常在co_spawn_entry_point中两次触发:
template <typename Handler, typename Executor, typename Function> awaitable<awaitable_thread_entry_point, Executor> co_spawn_entry_point( awaitable<void, Executor>*, co_spawn_state<Handler, Executor, Function> s) { (void) co_await co_spawn_dispatch{}; (co_await awaitable_thread_has_context_switched{}) = false; std::exception_ptr e = nullptr; try { // 第一次:调用用户协程时检测到取消并抛出,被catch捕获 co_await s.function(); } catch (...) { e = std::current_exception(); } bool switched = (co_await awaitable_thread_has_context_switched{}); if (!switched) (void) co_await co_spawn_post(); // 第二次:await_transform中再次检查取消,抛出异常未被捕获 (dispatch)(s.handler_work.get_executor(), [handler = std::move(s.handler), e]() mutable { std::move(handler)(e); }); }
解决方案
方案1:延迟触发取消信号
问题根源是协程尚未完成初始化就被取消,导致内部co_spawn_post阶段检测到取消状态并抛出未捕获异常。通过post延迟取消信号的触发,让用户协程先执行完毕:
void subscribe(ba::io_context &context) { ba::co_spawn( context, []() -> ba::awaitable<void> { co_return; }, ba::bind_cancellation_slot(cancel_sub.slot(), ba::detached)); // 用post将取消操作放入io_context队列,等待协程执行完成后再触发 ba::post(context, [&](){ cancel_sub.emit(ba::cancellation_type::all); }); }
方案2:自定义Handler捕获异常
替代默认的detached handler,自定义一个能捕获所有异常的handler,避免未处理异常终止程序:
// 自定义handler,捕获并处理异常 struct safe_detached { void operator()(std::exception_ptr e) const noexcept { if (e) { try { std::rethrow_exception(e); } catch (const std::exception& ex) { // 可根据需求处理异常,比如打印日志或直接忽略 // std::cerr << "Caught exception: " << ex.what() << std::endl; } } } }; void subscribe(ba::io_context &context) { ba::co_spawn( context, []() -> ba::awaitable<void> { co_return; }, ba::bind_cancellation_slot(cancel_sub.slot(), safe_detached{})); cancel_sub.emit(ba::cancellation_type::all); }
方案3:调整取消类型(可选)
如果业务允许,可将取消类型从all改为terminal或其他更严格的类型,减少内部触发异常的场景,但此方案不保证适用于所有情况:
cancel_sub.emit(ba::cancellation_type::terminal);
内容的提问来源于stack exchange,提问作者Jean-Michaël Celerier
相关产品推荐
相关产品推荐

