Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Asio's `io_context` and concurrency hints

The asio::io_context constructor takes an optional concurrency hint that skips some internal locks when only a single thread will interact with the io_context or associated IO objects (or synchronization between threads is already done in the calling code).

My understand is that 1 will allow me to call io_context::run() in one thread and interact normally with the io_context (i.e., all methods except reset() and run(), run_one() etc.) and all associated IO objects.

Additional, with ASIO_CONCURRENCY_HINT_UNSAFE_IO, calling any IO method on IO objects (number 3 in the example below) is illegal and calling any method on the io_context itself is illegal with ASIO_CONCURRENCY_HINT_UNSAFE. Is this correct?

#include <asio/io_context.hpp>
#include <asio/ip/tcp.hpp>
#include <chrono>
#include <iostream>
#include <thread>

static const char msg[] = "Hello World\n";

int main() {
    const auto concurrency_hint = ASIO_CONCURRENCY_HINT_1;
    asio::io_context ctx{concurrency_hint};
    asio::ip::tcp::acceptor acc(ctx, asio::ip::tcp::endpoint(asio::ip::address_v4::any(), 7999));
    acc.listen(2);

    asio::ip::tcp::socket peer(ctx);
    acc.async_accept(peer, [&peer](const asio::error_code &error) {
        // call async methods from the thread running the io context (1)
        peer.async_write_some(
            asio::const_buffer(msg, 12), [&](const asio::error_code &error, std::size_t len) {
                peer.close();
            });
    });

    std::thread io_thread([&ctx]() { ctx.run(); });

    // call `post()` from another thread (2)
    asio::post([]() { std::cout << msg << std::flush; });

    // call `async_accept` for an IO object running on another thread (3)
    acc.async_accept([&](const asio::error_code &error, asio::ip::tcp::socket peer) {
        peer.close();
    });

    // call `run()` while another thread is already doing so (4)
    ctx.run_for(std::chrono::seconds(2));

    std::this_thread::sleep_for(std::chrono::seconds(5));
    // Call `io_context::stop()` from another thread (5)
    ctx.stop();

    io_thread.join();
    return 0;
}
1 UNSAFE UNSAFE_IO SAFE
IO methods, same thread (1)
post() from another thread (2)
IO methods, another thread (3) ✔?
run() from two threads (4)
io_context::stop() from another thread (5) ?
like image 809
tstenner Avatar asked Nov 10 '21 19:11

tstenner


1 Answers

The ability to disable all synchronization was added to make the library consistent with all other "std" libraries, where the convention is to always make the caller deal with synchronization. This was for the "std::network" proposal. (If I remember correctly).

The more interesting cases are "1" and BOOST_ASIO_CONCURRENCY_HINT_UNSAFE_IO.

From the https://www.boost.org/doc/libs/1_66_0/doc/html/boost_asio/overview/core/concurrency_hint.html I can see that the value "1" uses thread-local storage for some work queues. This would mean that it effectively disrupts work-balancing. I would presume that this means that it will operate correctly when used with more threads, but less efficiently. Sorry I know this is wishy/washy, but it is not clear from the manual. The code definitely refers to it as a "hint" which generally means it will operate correctly but slower if it is set wrong.

The other value BOOST_ASIO_CONCURRENCY_HINT_UNSAFE_IO, is the same as full locking, except for the define ASIO_CONCURRENCY_HINT_LOCKING_REACTOR_IO. Again, it is not clear from the manual what this actually means, and I could not find any evidence it is ever used. (I will update this answer in a bit, after I check again on a later version of ASIO). However, from the description, it looks like a promise (not hint) that no two completion handlers/any async operations occur concurrently. The reactor engine is the bit that says "when this completes do this", so it would effect the registering of new async-callbacks and the removal of the callback after it has been called.

Sorry, I know this is not a good answer, but I did some research, and thought I might as well post it.


EDIT: More information

I had a look around boost/asio/1.77.0

The first learning point for me is that not all implementations use a lock-free (concurrency) queue. The windows version uses critical sections and windows events... TIL

However, I did find the relevant parts in the code. For example, the epoll version (see below). NOTE: epoll is not the default for linux io_uring is.

epoll_reactor::descriptor_state* epoll_reactor::allocate_descriptor_state()
{
  mutex::scoped_lock descriptors_lock(registered_descriptors_mutex_);
  return registered_descriptors_.alloc(BOOST_ASIO_CONCURRENCY_HINT_IS_LOCKING(
        REACTOR_IO, scheduler_.concurrency_hint()));
}

What this does is allocate a "state" with either a fake mutex or a real one. This is done via the conditional_mutex class.

The other interesting thing is that the flags interact with the hint. Switching the locking switches on the thread local queue optimizations, as below...


scheduler::scheduler(boost::asio::execution_context& ctx,
    int concurrency_hint, bool own_thread, get_task_func_type get_task)
  : boost::asio::detail::execution_context_service_base<scheduler>(ctx),
    one_thread_(concurrency_hint == 1
        || !BOOST_ASIO_CONCURRENCY_HINT_IS_LOCKING(
          SCHEDULER, concurrency_hint)
        || !BOOST_ASIO_CONCURRENCY_HINT_IS_LOCKING(
          REACTOR_IO, concurrency_hint)),
    mutex_(BOOST_ASIO_CONCURRENCY_HINT_IS_LOCKING(
          SCHEDULER, concurrency_hint)),
    task_(0),
    get_task_(get_task),
    task_interrupted_(true),
    outstanding_work_(0),
    stopped_(false),
    shutdown_(false),
    concurrency_hint_(concurrency_hint),
    thread_(0)
...

Nothing I found goes against my original answer though, disabling IO locking, seems to only remove locks from the reactor, which is used when an async function is called, or a completion handler is run.


Feedback to comments

@tstenner questioned whether io_uring was the default for linux. He is right to question,it turns I did not check, and it depends on the linux version. #if LINUX_VERSION_CODE >= KERNEL_VERSION(2,5,45)

He also asks

calling e.g. socket.async_write(…) while another thread is running a completion handler from within io_context::run() is problematic?

The answer is (from reading the code) yes. The same mutex is used to protect "start_op", which is used in many places, but specifically in basic_socket/acceptor async functions. So assuming the mutex was required for "start_op", the fact it is disabled would lead you to conclude there is potential U/B here.

A quick "grep" in the impl directory (for start_op) will give you a list of functions that I would avoid using. You can add to that list the timer async-functions, which appear to implemented in the reactor itself.

like image 75
Tiger4Hire Avatar answered Nov 14 '22 12:11

Tiger4Hire