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::futuresynchronization 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 distinctacceptorobjects 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 thecancelis 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?
 
    
