jeudi 27 octobre 2016

What handlers does boost.Asio execute behind the scenes?

I've got a very simple Boost.Asio case: an async_read protected by a deadline_timer. I also have a std::atomic_bool DEBUG[2]. The async_read handler sets DEBUG[0]; the deadline_timer sets DEBUG[1]. This happens unconditionally, even if the error code is error::operation_aborted.

Now, when I call io_service::run_one() I usually see either one of the DEBUG indicators set. However, in at least 10% of the cases, run_one returns 1 yet none of the two indicators are set i.e. neither of the two handlers was called. (Also the other side effects of the handler are missing).

Now run_one is supposed to return the number of handlers executed, so when it returns 1 it must have executed a handler - but which handler, if not mine?

The reason I ask is because even after a .reset(), the io_service object is broken.

Relevant code - rather verbose to make the problem clear:

boost::asio::deadline_timer deadline(thread_io_service);
deadline.expires_from_now(boost::posix_time::seconds(timeoutSeconds));
read_counter += 2; // Initialized to 1 in ctor, so always odd.
// C++11: Cannot capture expressions such as this->read_counter.
unsigned read_counter_copy = read_counter;
read_timeout.store(0, std::memory_order_release); // 0 = no timeout.
deadline.async_wait([&, read_counter_copy](boost::system::error_code const&)
    {
        // read_counter_copy is very intentionally captured by value - this timeout applies only to the next read.
        read_timeout.store(read_counter_copy, std::memory_order_release);
        DEBUG[0] = true;
    }
);

// Start reading "asynchronously", wait for completion or timeout:
std::atomic<boost::system::error_code> ec(boost::asio::error::would_block);
size_t len = 0;

boost::asio::async_read(socket, boost::asio::buffer(buffer + byteShift), boost::asio::transfer_exactly(nrBytes),
    [&](boost::system::error_code const& err, size_t bytesTransferred)
{
    len = bytesTransferred;
    ec.store(err, std::memory_order_release);
    DEBUG[1] = true;
}
);

// We only have 5 states to deal with
enum { pending, timeout, read, read_then_timeout, timeout_then_read } state = pending;
for (;;)
{
    if      (state == read_then_timeout) assert(false); // unreachable - breaks directly
    else if (state == timeout_then_read) assert(false); // unreachable - breaks directly
    // [pending, read, timeout] i.e. only one handler has run yet.
    thread_io_service.run_one(); // Don't trust this - check the actual handlers and update state accordingly.
    if (state == pending && read_timeout.load(std::memory_order_acquire) == read_counter)
    {
        state = timeout;
        socket.cancel(); // This will cause the read handler to be called with ec=aborted
        continue;
    }
    if (state == read && read_timeout.load(std::memory_order_acquire) == read_counter)
    {
        state = read_then_timeout;
        break; // 
    }
    if (state == pending && ec.load(std::memory_order_acquire) != boost::asio::error::would_block)
    {
        state = read;
        deadline.cancel();
        continue;
    }
    if (state == timeout && ec.load(std::memory_order_acquire) != boost::asio::error::would_block)
    {
        state = timeout_then_read; // Might still be a succesfull read (race condition)
        break;
    }
    // This is the actual problem: neither read nor timeout. 
    // DEBUG == {false,false} when this happens.
    L_NET(warning) << "Boost.Asio spurious return";
}
assert(state == timeout_then_read || state == read_then_timeout);

Aucun commentaire:

Enregistrer un commentaire