多线程应用程序上的 boost::asio::ssl 访问冲突

Posted

技术标签:

【中文标题】多线程应用程序上的 boost::asio::ssl 访问冲突【英文标题】:boost::asio::ssl access violation on multi-threaded application 【发布时间】:2013-09-06 11:36:27 【问题描述】:

我创建了一个程序,它利用 boost 的 ssl 实现使用async_read_some()async_write_some() 向远程服务器发送和接收小数据包。读取和写入被包裹在一个链中以防止并发读取和写入。此外,我为每个创建的包装函数都包含一个互斥锁,以进一步防止并发访问(可能过度杀伤,但不会造成伤害)。写入内容存储在写入队列中,当线程被通知数据可用时将其发送到该队列中。

我遇到的问题是按顺序执行大量写入时出现的,从而导致各种错误,例如 Second Chance Assertion Failed 和 Access Violation。我还收到“解密失败或坏记录 mac”的读取错误。

从我到目前为止所做的研究中,我发现如果同时执行读取和写入,SSL 套接字可能会损坏 - 至少根据讨论 here 和 here OP 的症状非常类似于我的。他还说他使用的链不起作用,但我不明白他的解决方案。由于我试图用来防止并发读取和写入的方法,这对于我遇到的问题是有意义的。

我一直在使用的一种解决方法是限制顺序写入,以便每次写入之间至少有 20 毫秒的间隔。比这少,我开始收到错误。这种解决方法不是很好,因为在某些时候可能仍然存在并发读/写导致错误。

这是我的读/写代码的摘要:

void client::write(std::string message, char packetType) 
    boost::lock_guard<boost::shared_mutex> lock(writeInProgressMutex);

    std::string preparedMessage = prepareWrite(message, packetType);
    char data_sent[2048];

    for(std::string::size_type i = 0; i < preparedMessage.size(); ++i) 
        data_sent[i] = preparedMessage[i];
        if (i + 1 == preparedMessage.size()) 
            data_sent[i+1] = NULL;
        
    

    socket_->async_write_some(boost::asio::buffer(data_sent), strand_.wrap(boost::bind(&client::handle_write, this, boost::asio::placeholders::error)));


void client::read() 
    boost::lock_guard<boost::shared_mutex> lock(readInProgressMutex);
    socket_->async_read_some(boost::asio::buffer(data_), strand_.wrap(boost::bind(&client::handle_read, this, boost::asio::placeholders::error)));

我已经尝试过不同类型的互斥锁,所以我认为这不是问题所在。如果有人知道如何确保我的链正常工作,或者您可以在我的代码/设计中看到一些明显的错误,请告诉我!

【问题讨论】:

您是否使用多个线程调用io_service::run()?如果是这样,当您使用单线程时问题会消失吗? 请更新您问题中的代码,说明您如何使用显式 strand 以确保最多有一个异步 ssl 操作未完成。 @SamMiller 我认为问题出在链上。我最初的实现是在读取操作仍在进行时允许多次写入。我现在正在修改一些代码,看看是否可以取得一些进展 - 如果我仍然遇到问题,我会报告我所做的更改。 【参考方案1】:

简而言之,验证对client::write()client::read() 的所有调用都在strand_ 内运行。如果没有额外的代码,诸如互斥锁之类的同步构造将无法有效防止并发操作被调用。


ssl::stream thread safety 的 Boost.Asio 文档强调了 strand 的要求:

不同的对象:安全。

共享对象:不安全。应用程序还必须确保所有异步操作都在相同的隐式或显式链中执行。

如果ssl::stream 操作仅在单个线程中调用,那么它是安全的,因为它们在隐式链中运行。另一方面,如果多个线程在ssl::stream 上调用操作,那么它们必须显式地与链同步。由于没有引入自定义asio_handler_invoke 函数的中间处理程序,其他同步构造(例如互斥锁)将无效。

这是一个流程图,显示了异步写入链的必要链使用:

void client::start_write_chain()

  strand_.post(boost::bind(&client::write, ...)) ---.
    .----------------------------------------------'
     |  .-------------------------------------------.
     V  V                                           |
void client::write(...)                             |
                                                   |
  ...                                               |
  socket_->async_write_some(buffer, strand_.wrap(   |
    boost::bind(&client::handle_write, ...)));  --. | 
    .--------------------------------------------' |
     V                                              |
void client::handle_write(                          |
  boost::system::error_code& error,                 |
  std::size_t bytes_transferred)                    |
                                                   |
  // If there is still data remaining.              |
  if (bytes_transferred < all_data)                 |
    write(...);  -----------------------------------'

此要求是实现细节的结果,即ssl::stream 异步操作是根据组合操作实现的。初始操作可能发生在调用者的上下文中,因此需要从strand_ 中调用client::write()。这些组合操作可能会导致对socket_ 的许多中间调用。由于这些中间调用不知道writeInProgressMutex,互斥体对于保护socket_ 变得无效。考虑阅读this 答案以了解有关组合操作的更多详细信息,以及阅读strands

此外,在client::write() 内,缓冲区data_sent 的生命周期无法满足ssl::stream::async_write_some() 的要求,其中指出:

虽然缓冲区对象可以根据需要被复制,但底层缓冲区的所有权由调用者保留,它必须保证它们在调用处理程序之前保持有效。

在这种情况下,data_sent 的生命周期在 client::write() 返回时结束,这可能发生在调用完成处理程序之前。

【讨论】:

是的,read() 没有从strand_ 中调用是一个问题,导致并发读取和写入。我错误地认为,由于async_connect()async_handshake() 是通过strand_ 调用的,所以async_read() 也会如此。添加strand_.post(&amp;client::read()) 似乎解决了这个问题!谢谢你。你能解释一下我如何保证 data_sent 的生命周期吗? @user2753546:考虑将data_sent 设为client 的成员变量,或者在空闲存储区分配和管理缓冲区。

以上是关于多线程应用程序上的 boost::asio::ssl 访问冲突的主要内容,如果未能解决你的问题,请参考以下文章

Elastic Beanstalk 上的 Tomcat - Tomcat 线程中的多线程

挂在 _dl_sysinfo_int80 上的多线程应用程序

android 上的 SQLite 数据库、多线程、锁和帐户同步

VS2010 上的基本 C++ 多线程示例

多线程应用程序上的 boost::asio::ssl 访问冲突

解释 gperftools 在多线程工作负载上的结果