Boost :: beast:多个async_write调用正在触发断言错误

时间:2018-06-06 19:21:27

标签: c++ boost websocket boost-asio boost-beast

我正在为我的全双工服务器编写测试,当我进行多个(顺序)async_write调用时(虽然用一个链覆盖),我从boost::beast得到以下断言错误文件boost/beast/websocket/detail/stream_base.hpp

// If this assert goes off it means you are attempting to
// simultaneously initiate more than one of same asynchronous
// operation, which is not allowed. For example, you must wait
// for an async_read to complete before performing another
// async_read.
//
BOOST_ASSERT(id_ != T::id);

要在您的计算机上重现该问题:可以找到重现此问题的完整客户端代码(MCVE)here。它在链接中不起作用,因为你需要一台服务器(在你自己的机器上,对不起,因为它不可能在网上方便地做到这一点,这更客观地表明问题出在客户端,而不是服务器中我把它包括在这里)。我用websocketd创建了一个命令为./websocketd --ssl --sslkey /path/to/server.key --sslcert /path/to/server.crt --port=8085 ./prog.py的服务器,其中./prog.py是一个简单的python程序,可以打印和刷新(我是从websocketd home page得到的)。

在客户端进行写作的调用如下所示:

  std::vector<std::vector<std::future<void>>> clients_write_futures(
      clients_count);
  for (int i = 0; i < clients_count; i++) {
    clients_write_futures[i] = std::vector<std::future<void>>(num_of_messages);
    for (int j = 0; j < num_of_messages; j++) {
      clients_write_futures[i][j] =
          clients[i]->write_data_async_future("Hello"); // writing here
    }
  }

请注意,我在示例中仅使用了一个客户端。客户端阵列只是在测试时对服务器造成更大压力的概括。

我对这个问题的评论:

  1. 循环是顺序的;它不像我在多线程中这样做
  2. 应该可以以全双工形式进行通信,其中向服务器发送无限数量的消息。如何进行全双工通信?
  3. 我正在使用strands来包装我的异步调用,以防止通过io_service / io_context
  4. 在套接字中发生任何冲突
  5. 使用调试器进行调查显示循环的第二次迭代一致地失败,这意味着我做了一些根本错误的事情,但我不知道它是什么。换句话说:这显然是一个确定性问题。
  6. 我在这里做错了什么?如何向websocket服务器写入无限数量的消息?

    编辑:

    Sehe,我想开始为代码混乱道歉(没有意识到它那么糟糕),并感谢你为此付出的努力。我希望你问我为什么它以这种(可能)有组织和混乱的方式同时构建,答案很简单:主要是一个gtest代码,看看我的通用,多功能websocket客户端是否正常工作,我用来强调 - 测试我的服务器(使用大量的多线程io_service对象,我认为这些对象很敏感,需要进行广泛的测试)。我打算在实际生产测试中同时用许多客户端轰炸我的服务器。我发布这个问题是因为客户的行为我不明白。我在这个文件中所做的是创建一个MCVE(人们一直在SO上请求)。我花了两个小时来剥离我的代码来创建它,最后我复制了我的gtest测试夹具代码(这是服务器上的一个夹具)并将其粘贴在主服务器中并验证问题仍然存在于另一台服务器上并清理干净一点点(显然结果不够)。

    那么为什么我不捕捉异常呢?因为gtest会抓住它们并认为测试失败了。主要不是生产代码,而是客户端。我从你提到的内容中学到了很多东西,我不得不说扔掉和捕获它是愚蠢的,但我不知道std :: make_exception_ptr(),所以我找到了我的(dumm)方法来实现相同的结果: - )。为什么有太多无用的函数:在这个测试/示例中它们没有用处,但通常我可以在以后将它们用于其他事情,因为这个客户端不仅仅适用于这种情况。

    现在回到问题:我不明白的是为什么我们必须用strand_覆盖async_write当它在主线程的循环中顺序使用时(我misexpressed我覆盖了仅处理程序)。我理解为什么处理程序被覆盖,因为套接字不是线程安全的,多线程io_service会在那里创建一个竞赛。我们也知道io_service::post本身是线程安全的(这就是为什么我认为不需要包装async_write)。你能解释一下,在做这个时我们需要包装async_write本身是不是线程安全的吗?我知道你已经知道了这一点,但同样的断言仍在解雇。我们对处理程序和异步排队进行了顺序化,客户端仍然不满意进行多次写入调用。还有什么可以遗漏?

    (顺便说一句,如果你写,然后得到未来,然后阅读,然后再写,它的工作原理。这就是为什么我使用期货,准确定义测试用例并定义测试的时间顺序。我'我在这里偏执。)

1 个答案:

答案 0 :(得分:2)

用一条链覆盖你的async_write。但 你不做这样的事情 。您可以看到的只是将完成处理程序包装在该链中。但是您直接发布了的异步操作

更糟糕的是,您正在从主线程执行此操作,而在与您的WSClient实例关联的任何线程上都存在异步操作,这意味着您同时访问非线程的对象实例-safe。

这是一场数据竞赛,所以你得到Undefined Behaviour

天真的修复可能是:

std::future<void> write_data_async_future(const std::string &data) {
    // shared_ptr is used to ensure data's survival
    std::shared_ptr<std::string> data_ptr = std::make_shared<std::string>(data);
    std::shared_ptr<std::promise<void> > write_promise = std::make_shared<std::promise<void> >();

    post(strand_, [=,self=shared_from_this()] {
        websock.async_write(
            boost::asio::buffer(*data_ptr),
            boost::asio::bind_executor(strand_, std::bind(&WSClientSession::on_write_future, self,
                                                          std::placeholders::_1, std::placeholders::_2, data_ptr,
                                                          write_promise)));
    });

    return write_promise->get_future();
}

但那还不够。现在您可以确保您的异步操作或其完成不会同时运行,但您仍然可以在调用第一个完成处理程序之前发布下一个异步操作。

要解决此问题,您只需排队。

说实话,我不确定你为什么如此关注使用期货的同步。这使得实现这一点变得非常困难。如果你可以描述你/功能/想要实现的目标,我可以提出一个可能更短的解决方案。

代码审查说明

在我明白代码的全部内容之前,我花了很多时间阅读你的代码。我不想让你忘记我在路上留下的笔记。

  

警告:这是一段旷日持久的代码潜水。我提供它是因为一些见解可能会帮助您了解如何重新构建代码。

我开始阅读异步代码链,直到on_handshake设置 started_promise值)。

然后我走向你的main功能的malstrom。你的主要功能是50行代码?!有几个并行容器和重复的手动嵌套循环?

这是我在重构之后得到的:

int main() {
    std::vector<actor> actors(1);

    for (auto& a : actors) {
        a.client = std::make_shared<WSClient>();
        a.session_start_future = a.client->start("127.0.0.1", "8085");
        a.messages.resize(50);
    }

    for (auto& a : actors) { a.session_start_future.get(); }

    for (auto& a : actors) { for (auto& m : a.messages) {
        m.write_future = a.client->write_data_async_future("Hello");
    } }

    for (auto& a : actors) { for (auto& m : a.messages) {
        m.read_future = a.client->read_data_async_future();
    } }

    for (auto& a : actors) { for (auto& m : a.messages) {
        m.write_future.get();
        std::string result = m.read_future.get();
    } }
}

所有数据结构都已折叠成小助手actor

struct actor {
    std::shared_ptr<WSClient> client;
    std::future<void> session_start_future;

    struct message {
        std::string message = GenerateRandomString(20);
        std::future<void> write_future;
        std::future<std::string> read_future;
    };

    std::vector<message> messages;
};
  

我们现在大约需要一小时的代码审查,没有任何收获,除了我们现在可以告诉main正在做什么,并且有信心说没有一些微不足道的错误。循环变量或其他东西。

领回

在写作开始时:write_data_async_future。等待。还有write_data_asyncwrite_data_sync。为什么?你想要阅读

更糟糕的是,WSClient仅将这些内容转发给假定的会话。为什么此时WSClientWSClientSession之间存在区别?我说,没有。

进一步消除了30行不那么有用的代码,我们仍然有同样的失败,所以这很好。

我们在哪里。 write_data_async_future。哦,是的,我们需要非未来版本吗?不,那么,还有40行代码消失了。

现在,真实:write_data_async_future

std::future<void> write_data_async_future(const std::string &data) {
    // shared_ptr is used to ensure data's survival
    std::shared_ptr<std::string> data_ptr = std::make_shared<std::string>(data);
    std::shared_ptr<std::promise<void> > write_promise = std::make_shared<std::promise<void> >();
    websock.async_write(
        boost::asio::buffer(*data_ptr),
        boost::asio::bind_executor(strand_, std::bind(&WSClientSession::on_write_future, shared_from_this(),
                                                      std::placeholders::_1, std::placeholders::_2, data_ptr,
                                                      write_promise)));
    return write_promise->get_future();
}

看起来......好吧。等等,有on_write_future?这可能意味着我们需要蒸发更多未使用的代码行。看......是的。噗,走了。

  

到目前为止,diffstat看起来像这样:

  test.cpp | 683 +++++++++++++++++++++++----------------------------------------
  1 file changed, 249 insertions(+), 434 deletions(-)

回到那个功能,让我们看一下on_write_future

void on_write_future(boost::system::error_code ec, std::size_t bytes_transferred,
                     std::shared_ptr<std::string> data_posted,
                     std::shared_ptr<std::promise<void> > write_promise) {
    boost::ignore_unused(bytes_transferred);
    boost::ignore_unused(data_posted);

    if (ec) {
        try {
            throw std::runtime_error("Error thrown while performing async write: " + ec.message());
        } catch (...) {
            write_promise->set_exception(std::current_exception());
        }
        return;
    }
    write_promise->set_value();
}

一些问题。通过的一切都被忽略了。我知道你传递了shared_ptrs的内容,但也许你应该将它们作为操作对象的一部分传递,以避免有这么多单独的共享ptrs。

抛出异常只是为了抓住它?嗯。对此我不确定。也许只是设置一个新的例外:

if (ec) {
    write_promise->set_exception(
            std::make_exception_ptr(std::system_error(ec, "async write failed")));
} else {
    write_promise->set_value();
}

即便如此,现在还存在一个概念问题。这种方式使用get()而无需捕获main这意味着任何连接中的任何错误都将中止所有操作。让错误简单地中止一个连接/会话/客户端是非常有用的。在您的代码中,这些代码都是同义词(以及io_contextthread)。

旁注:您将线程存储为成员,但始终将其分离。这意味着从那时起该成员就没用了。

  

在这一点上,我从审查中休息了一下,然后我发现了脑电波向我展示了这个问题。我锻炼的半结果是here。请注意,您无法使用它,因为它实际上不能解决问题。但它可能在其他方面有所帮助吗?