【发布时间】:2021-07-01 08:11:14
【问题描述】:
我正在 Windows 上使用 Mingw64 编译最新版本的 ASIO。
我有一个用于接受 tcp 连接的沙箱代码。我使用一个上下文,每个接受器一个链和一个套接字和 2 个线程(我在文档中读到,发布到两个不同的链中并不能保证并发调用)。由于某种原因,我在执行结束时遇到了死锁,我不知道为什么会发生这种情况。如果出现以下情况,则不会发生:
- 我使用 1 个线程和一个公共上下文
- 有时当我使用 1 个上下文和 2 个没有线程的线程时
- 我使用 2 个不同的上下文和 2 个没有链的不同线程
- 在
std::future同步和请求停止服务器之间经过一段时间 -
有时当我明确地将
acceptor.cancel()发布给它的执行者时
如果我close接受者,也不会发生死锁。
我未能在文档中找到任何可能解释此类行为原因的相关信息。而且我不想忽略它,因为它可能会导致无法预料的问题。
这是我的沙盒代码:
#include <asio.hpp>
#include <iostream>
#include <sstream>
#include <functional>
constexpr const char localhost[] = "127.0.0.1";
constexpr unsigned short port = 12000;
void runContext(asio::io_context &io_context)
{
std::string threadId{};
std::stringstream ss;
ss << std::this_thread::get_id();
ss >> threadId;
std::cout << std::string("New thread for asio context: ")
+ threadId + "\n";
std::cout.flush();
io_context.run();
std::cout << std::string("Stopping thread: ")
+ threadId + "\n";
std::cout.flush();
};
class server
{
public:
template<typename Executor>
explicit server(Executor &executor)
: acceptor_(executor)
{
using asio::ip::tcp;
auto endpoint = tcp::endpoint(asio::ip::make_address_v4(localhost),
port);
acceptor_.open(endpoint.protocol());
acceptor_.set_option(tcp::acceptor::reuse_address(true));
acceptor_.bind(endpoint);
acceptor_.listen();
}
void startAccepting()
{
acceptor_.async_accept(
[this](const asio::error_code &errorCode,
asio::ip::tcp::socket peer)
{
if (!errorCode)
{
startAccepting();
// std::cout << "Connection accepted\n";
}
if (errorCode == asio::error::operation_aborted)
{
// std::cout << "Stopped accepting connections\n";
return;
}
});
}
void startRejecting()
{
// TODO: how to reject?
}
void stop()
{
// asio::post(acceptor_.get_executor(), [this](){acceptor_.cancel();}); // this line fixes deadlock
acceptor_.cancel();
// acceptor_.close(); // this line also fixes deadlock
}
private:
asio::ip::tcp::acceptor acceptor_;
};
int main()
{
setvbuf(stdout, NULL, _IONBF, 0);
asio::io_context context;
// run server
auto serverStrand = asio::make_strand(context);
server server{serverStrand};
server.startAccepting();
// run client
auto clientStrand = asio::make_strand(context);
asio::ip::tcp::socket socket{clientStrand};
size_t attempts = 1;
auto endpoint = asio::ip::tcp::endpoint(
asio::ip::make_address_v4(localhost), port);
std::future<void> res = socket.async_connect(endpoint, asio::use_future);
std::future<void> runningContexts[] = {
std::async(std::launch::async, runContext, std::ref(context)),
std::async(std::launch::async, runContext, std::ref(context))
};
res.get();
server.stop();
std::cout << "Server has been requested to stop" << std::endl;
return 0;
}
更新
根据sehe的回答,我遇到了死锁,因为当调用server.stop() 时,已经发布了成功接受的完成处理程序,但由于取消从未调用过,这导致上下文有待处理的工作,因此在结束(如果我理解正确的话)。
我仍然不明白的事情是:
- 服务器有一个单独的分支(根据规范)强制接受者的命令以非并发和先进先出的顺序被调用。没有提供执行程序的处理程序也必须在同一个线程中处理。文档中没有关于
acceptor::cancel()方法的线程安全性,尽管不同的acceptor对象是安全。所以我假设它是线程安全的(在一个strand内不可能有数据竞争)。 如果cancel通过asio::post显式发布到acceptor的线程中,@sehe 的代码不会导致死锁。对于 500 次调用,没有死锁:
test 499
Awaiting client
New thread 3
New thread 2
Completed client
Server stopped
Accept: 127.0.0.1:14475
Accept: The I/O operation has been aborted because of either a thread exit or an application request.
Stopping thread: 2
Stopping thread: 3
Everyting shutdown
但是,如果我在同步之前删除打印代码和导致延迟的stop(),则很容易实现死锁:
PS C:\dev\builds\asio_connection_logic\Release-MinGW-w64\bin> for ($i = 0; $i -lt 500; $i++){
>> Write-Output "
>> test $i"
>> .\sb.sf_example.exe}
test 0
New thread 2
New thread 3
Server stopped
Accept: 127.0.0.1:15160
PS C:\dev\builds\asio_connection_logic\Release-MinGW-w64\bin>
PS C:\dev\builds\asio_connection_logic\Release-MinGW-w64\bin> for ($i = 0; $i -lt 500; $i++){
>> Write-Output "
>> test $i"
>> .\sb.sf_example.exe}
test 0
New thread 2New thread 3
Server stopped
Accept: 127.0.0.1:15174
PS C:\dev\builds\asio_connection_logic\Release-MinGW-w64\bin> ^C
所以,结论是不管你怎么调用acceptor.cancel(),都会死锁。
- 有没有办法避免acceptor死锁?
【问题讨论】:
-
我必须编辑你的代码才能真正使用 boost asio。您是要标记独立的 Asio 吗?
-
@sehe 我使用了独立的 asio,但标记了 boost,因为有更多标签
-
知道了。我不怪你。幸运的是,无论如何诊断都是一样的。
标签: c++ multithreading boost-asio asio