I am compiling with Mingw64 on Windows the latest version of ASIO.
I have a sandbox code for accepting tcp connections. I use one context, a strand per acceptor and a socket and 2 threads (I have read in the documentation that posting into two different strands does not guarantee concurrent invocation). For some reason I get a deadlock at the end of execution and I don't know why it happens. It does not happen if:
- I use 1 thread and one common context
- Sometimes when I use 1 context and 2 threads without strands
- I use 2 different contexts with 2 different threads without strands
- when some time passes between
std::future
synchronization and a request to stop a server - Sometimes when I post
acceptor.cancel()
to its executor explicitly
Deadlock also does not happen if I close
the acceptor.
I have failed to find any relevant info in the documentation which might explain the reason for such behavior. And I don't want to ignore it since it might result in unpredictable problems.
Here is my sandbox code:
#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;
}
UPDATE
According to the sehe's answer I am getting a deadlock, because when server.stop()
is invoked, completion handler for successful acception has been already posted but due to cancellation is never invoked, which causes a context to have pending work, hence a deadlock at the end (if I understood correctly).
The things I still don't understand are:
- There is a separate strand for the server which (according to specification) enforces acceptor's commands to be invoked non-concurrently and in FIFO order. Handlers with no provided executors also have to be handled in the same thread. There's nothing about thread safety of
acceptor::cancel()
method in documentation, though distinctacceptor
objects are safe. So I assumed that it is thread safe (no data races possible within onestrand
). @sehe's code does not cause deadlock in case thecancel
is explicitly posted into theacceptor
's thread viaasio::post
. For 500 invokation there were no deadlocks:
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
However, if I remove printing code before synchronization and stop()
which causes a delay, a dead lock is easily achievable:
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
So, the conclusion is that no matter how you invoke acceptor.cancel()
, you will get a deadlock.
- Is there even a way to avoid deadlock for acceptor?