Boost.Beast WebSocket析构函数中async_close失败报“Operation canceled”且阻塞的问题
看起来你在OKX的WebSocket客户端析构里处理优雅关闭时踩了Boost.Asio异步编程的常见坑,我来帮你拆解问题和给出解决方案。
先看你的问题:析构函数里调用async_close后用std::promise等待完成,结果要么卡在future.get()永远阻塞,要么手动加m_ioc.run()就报“Operation canceled”错误。这两个问题的根源都是异步操作的生命周期和io_context的线程状态没处理对。
问题根源分析
为什么
future.get()会阻塞?
你的WebSocket对象的strand绑定在m_ioc上,而运行m_ioc.run()的线程如果已经因为没有待处理的异步任务而退出了(比如之前的订阅、读写操作都结束了),那你发起的async_close的handler根本没机会被执行。std::promise一直得不到set_value(),future.get()自然就无限阻塞了。为什么加
m_ioc.run()会报“Operation canceled”?
如果你的unsubscribeAllSync()已经让WebSocket进入了非活跃状态,或者之前的操作间接导致m_ws内部状态不允许关闭,再加上直接调用m_ioc.run()可能和strand的调度逻辑冲突,就会触发操作被取消的错误。另外你原代码里m_ioc.stop()的顺序完全反了——stop()是用来终止io_context的事件循环,应该在join线程之前调用。
正确的解决方案:不要在析构里处理异步关闭
析构函数是同步执行的,而异步操作依赖io_context的事件循环,两者的生命周期很难匹配,这本身就不符合异步编程的最佳实践。我推荐你换个思路:提供一个显式的shutdown方法,让用户在销毁对象前主动调用,等待异步关闭完成后再释放对象。
下面是修改后的核心代码示例:
class okx : public std::enable_shared_from_this<okx> { private: std::atomic<bool> m_shuttingDown{false}; // 标记是否正在关闭 public: // 显式的shutdown方法,返回future让调用者等待关闭完成 std::future<void> shutdown() { std::promise<void> promise; auto future = promise.get_future(); // 先标记正在关闭,阻止新的读写操作发起 m_shuttingDown = true; // 同步取消所有订阅(确保没有新的异步请求发出) unsubscribeAllSync(); // 必须把close操作dispatch到WebSocket的strand上,保证线程安全 net::dispatch(m_ws.get_executor(), [self = shared_from_this(), promise = std::move(promise)]() mutable { self->m_ws.async_close(websocket::close_code::normal, [self = std::move(self), promise = std::move(promise)](beast::error_code ec) mutable { if (ec) { std::cerr << "WebSocket关闭失败: " << ec.message() << std::endl; } // 完成promise,通知调用者关闭结束 promise.set_value(); }); }); return future; } ~okx() { // 析构里只做简单清理,不要碰异步操作 std::cout << "OKX WebSocket客户端已销毁" << std::endl; } // 其他方法比如read的handler里要检查m_shuttingDown,避免继续操作 void on_read(beast::error_code ec, std::size_t bytes_transferred) { boost::ignore_unused(bytes_transferred); if (m_shuttingDown || ec) { return; // 正在关闭或者出错,直接返回 } // 正常的读处理逻辑... } };
然后你的客户端使用方式要改成这样:
// 创建客户端 auto okx_ws = std::make_shared<exchangeConnector::Ws::okx>(&your_read_queue); okx_ws->start(); // ... 业务逻辑 ... // 要关闭的时候,先调用shutdown等待完成 auto shutdown_future = okx_ws->shutdown(); shutdown_future.get(); // 阻塞直到关闭完成 // 现在再销毁对象 okx_ws.reset();
如果你非要在析构里处理(不推荐)
如果因为架构限制必须在析构里处理,那你得确保io_context在async_close完成前一直处理事件,同时所有操作都在strand上执行:
~okx() { std::cout << "1" << std::endl; unsubscribeAllSync(); std::cout << "2" << std::endl; beast::error_code close_ec; std::atomic<bool> close_completed{false}; // 把close操作dispatch到strand上,确保在io_context的事件循环里执行 net::dispatch(m_ws.get_executor(), [this, &close_ec, &close_completed]() { m_ws.async_close(websocket::close_code::normal, [&close_ec, &close_completed](beast::error_code ec) { close_ec = ec; close_completed = true; }); }); // 循环运行io_context,直到close完成 while (!close_completed) { m_ioc.run_one(); // 每次处理一个事件 } if (close_ec) { std::cerr << "关闭失败: " << close_ec.message() << std::endl; } std::cout << "3" << std::endl; // 先停止io_context,再join线程(顺序很重要!) m_ioc.stop(); for (auto& thread : m_threads) { if (thread.joinable()) { thread.join(); } } std::cout << "4" << std::endl; }
但这个方案有风险:如果close_completed永远不会变成true(比如网络彻底断开),你的程序会一直卡在析构里。所以还是强烈推荐用显式shutdown的方案。
额外的注意事项
- 所有对WebSocket的操作(包括读、写、关闭)都必须在它的strand上执行,用
net::dispatch或者net::post到m_ws.get_executor(),避免线程安全问题。 - 用
std::move传递std::promise到handler里,绝对不要用局部变量的引用——一旦析构函数执行完,引用就会失效,导致未定义行为。 - 在shutdown过程中,一定要阻止新的异步操作发起,比如用
m_shuttingDown原子标志在所有handler里检查。
内容来源于stack exchange




