1
votes

I am looking at the Boost Asio Blocking TCP Client timeout example with a special interest on how connection timeouts are implmented. How do we know from the documentation that the callback handler and subsequent checks don't introduce a race condition?

The Asynchronous connection command

boost::asio::async_connect(socket_, iter, var(ec) = _1);

executes the var(ec) = _1 which is the handler for setting the error code once execute. Alternatively, a full and explicit lambda could be used here.

At the same time, the check_deadline function appears to be called by the deadline_ member. The timeout appears to be enforced by having the deadline forcibly close the socket whereup we assume that perhaps that the blocking statement

do io_service_.run_one(); while (ec == boost::asio::error::would_block);

would return. At first I thought that the error code must be atomic but that doesn't appear to be the case. Instead, this page, appears to indicate that the strand model will work whenever the calls to the socket/context come from the same thread.

So we assume that each callback for the deadline (which is in Asio) and the handle for the async_connect routine will not be run concurrently. Pages such as this in the documentation hint that handlers will only execute during run() calls which will prevent the command while(ec == whatever) from behind executed during the handler currently changing its value.

How do I know this explicitly? What in the documentation that tells me explicitly that no handlers will ever execute outside these routines? If true, the page on the proactor design pattern must infer this, but never explicitly where the "Initiator" leads to the "Completion Handler".

The closes I've found is the documentation for io_context saying

Synchronous operations on I/O objects implicitly run the io_context object for an individual operation. The io_context functions run(), run_one(), run_for(), run_until(), poll() or poll_one() must be called for the io_context to perform asynchronous operations on behalf of a C++ program. Notification that an asynchronous operation has completed is delivered by invocation of the associated handler. Handlers are invoked only by a thread that is currently calling any overload of run(), run_one(), run_for(), run_until(), poll() or poll_one() for the io_context.

This implies that if I have one thread running the run_one() command then its control path will wait until a handler is available and eventually wind its way through a handler whereupon it will return and check ther ec value.

Is this correct and is "Handlers are invoked only by a thread that is currently calling any overload of run(), run_one(), run_for(), run_until(), poll() or poll_one() for the io_context." the best statement to find for understanding how the code will always function? Is there any other exposition?

1

1 Answers

1
votes

The Asio library is gearing up to be standardized as NetworkingTS. This part is indeed the deal:

Handlers are invoked only by a thread that is currently calling any overload of run(), run_one(), run_for(), run_until(), poll() or poll_one() for the io_context

You are correct in concluding that the whole example is 100% single-threaded¹. There cannot be a race.

I personally feel the best resource is the Threads and Boost.Asio page:

  • By only calling io_context::run() from a single thread, the user's code can avoid the development complexity associated with synchronisation. For example, a library user can implement scalable servers that are single-threaded (from the user's point of view).

It also reiterates the truth from earlier:

[...] the following guarantee:

  • Asynchronous completion handlers will only be called from threads that are currently calling io_context::run().

¹ except potential internal service threads depending on platform/extensions, as the threads page details