You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

Boost.Beast WebSocket析构函数中async_close失败报“Operation canceled”且阻塞的问题

Boost.Beast WebSocket析构函数中async_close失败报“Operation canceled”且阻塞的问题

看起来你在OKX的WebSocket客户端析构里处理优雅关闭时踩了Boost.Asio异步编程的常见坑,我来帮你拆解问题和给出解决方案。

先看你的问题:析构函数里调用async_close后用std::promise等待完成,结果要么卡在future.get()永远阻塞,要么手动加m_ioc.run()就报“Operation canceled”错误。这两个问题的根源都是异步操作的生命周期和io_context的线程状态没处理对

问题根源分析

  1. 为什么future.get()会阻塞?
    你的WebSocket对象的strand绑定在m_ioc上,而运行m_ioc.run()的线程如果已经因为没有待处理的异步任务而退出了(比如之前的订阅、读写操作都结束了),那你发起的async_close的handler根本没机会被执行。std::promise一直得不到set_value()future.get()自然就无限阻塞了。

  2. 为什么加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的方案。

额外的注意事项

  1. 所有对WebSocket的操作(包括读、写、关闭)都必须在它的strand上执行,用net::dispatch或者net::postm_ws.get_executor(),避免线程安全问题。
  2. std::move传递std::promise到handler里,绝对不要用局部变量的引用——一旦析构函数执行完,引用就会失效,导致未定义行为。
  3. 在shutdown过程中,一定要阻止新的异步操作发起,比如用m_shuttingDown原子标志在所有handler里检查。

内容来源于stack exchange

火山引擎 最新活动