多线程应用程序上的 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(&client::read())
似乎解决了这个问题!谢谢你。你能解释一下我如何保证 data_sent 的生命周期吗?
@user2753546:考虑将data_sent
设为client
的成员变量,或者在空闲存储区分配和管理缓冲区。以上是关于多线程应用程序上的 boost::asio::ssl 访问冲突的主要内容,如果未能解决你的问题,请参考以下文章
Elastic Beanstalk 上的 Tomcat - Tomcat 线程中的多线程
挂在 _dl_sysinfo_int80 上的多线程应用程序
android 上的 SQLite 数据库、多线程、锁和帐户同步