1

Do boost::asio c++20 coroutines support multithreading?

The boost::asio documentation examples are all single-threaded, are there any multithreaded examples?

Joris Timmermans
  • 10,814
  • 2
  • 49
  • 75
AeroSun
  • 2,401
  • 2
  • 23
  • 46
  • The whole point of coroutines is to be single threaded.That said, coroutines could be executed by multiple threads but the usual principles of synchronization need to be respected. – Something Something Jan 20 '23 at 13:15
  • @NoleKsum, I agreed with you. But I don't found how to launch coroutines by multiple thread in boost::asio c++20 examples. Could you show please? – AeroSun Jan 20 '23 at 13:25
  • @AeroSun: What examples are you looking at? – Nicol Bolas Jan 20 '23 at 15:18
  • @AeroSun Because it is unnecessary. You can start 10 threads and on each one you start one thousand coroutines, but that does not prove anything. Coroutines are inherently to be used single threaded. That is the whole point. – Something Something Jan 20 '23 at 16:38
  • @NoleKsum, Hmmm, there are solved other question. Ok, let's I asked from other side. Here (https://www.boost.org/doc/libs/1_81_0/doc/html/boost_asio/example/cpp20/coroutines/echo_server.cpp) example with coroutines. As I guess - it is single threaded. What should be changed in the example for utilize all CPUs? – AeroSun Jan 21 '23 at 12:26

1 Answers1

1

Yes.

In Asio, if multiple threads run execution context, you don't normally even control which thread resumes your coroutine.

You can look at some of these answers that ask about how to switch executors mid-stream (controlling which strand or execution context may resume the coro):


Update to the comment:

To make the c++20 coro echo server sample multi-threading you could change 2 lines:

boost::asio::io_context io_context(1);
// ...
io_context.run();

Into

boost::asio::thread_pool io_context;
// ...
io_context.join();

Since each coro is an implicit (or logical) strand, nothing else is needed. Notes:

  • Doing this is likely useless, unless you're doing significant work inside the coroutines, that would slow down IO multiplexing on a single thread.
  • In practice a single thread can easily handle 10k concurrent connections, especially with C++20 coroutines.
  • Note that it can be a significant performance gain to run the asio::io_context(1) with the concurrency hint, because it can avoid synchronization overhead.
  • When you introduce e.g. asynchronous session control or full-duplex you will have the need for an explicit strand. In the below example I show how you would make each "session" use a strand, and e.g. do graceful shutdown.

Live On Coliru

#include <boost/asio.hpp>
#include <boost/asio/co_spawn.hpp>
#include <boost/asio/experimental/awaitable_operators.hpp>
#include <iostream>
#include <list>

namespace asio = boost::asio;
namespace this_coro = asio::this_coro;
using boost::system::error_code;
using asio::ip::tcp;
using asio::detached;
using executor_type = asio::any_io_executor;
using socket_type   = asio::use_awaitable_t<>::as_default_on_t<tcp::socket>; // or tcp::socket
                                                                             //
using session_state = std::shared_ptr<socket_type>;                          // or any additional state
using handle        = std::weak_ptr<session_state::element_type>;

using namespace std::string_view_literals;
using namespace asio::experimental::awaitable_operators;

asio::awaitable<void> echo_session(session_state s) {
    try {
        for (std::array<char, 1024> data;;) {
            size_t n = co_await s->async_read_some(asio::buffer(data));
            co_await async_write(*s, asio::buffer(data, n));
        }
    } catch (boost::system::system_error const& se) {
        if (se.code() != asio::error::operation_aborted) // expecting cancellation
            throw;
    } catch (std::exception const& e) {
        std::cout << "echo Exception: " << e.what() << std::endl;
        co_return;
    }

    error_code ec;
    co_await async_write(*s, asio::buffer("Server is shutting down\n"sv),
                         redirect_error(asio::use_awaitable, ec));

    // std::cout << "echo shutdown: " << ec.message() << std::endl;
}

asio::awaitable<void> listener(std::list<handle>& sessions) {
    auto ex = co_await this_coro::executor;

    for (tcp::acceptor acceptor(ex, {tcp::v4(), 55555});;) {
        session_state s = std::make_shared<socket_type>(
            co_await acceptor.async_accept(make_strand(ex), asio::use_awaitable));

        sessions.remove_if(std::mem_fn(&handle::expired)); // "garbage collect", optional
        sessions.emplace_back(s);

        co_spawn(ex, echo_session(s), detached);
    }
}

int main() {
    std::list<handle> handles;

    asio::thread_pool io_context;
    asio::signal_set signals(io_context, SIGINT, SIGTERM);

    auto handler = [&handles](std::exception_ptr ep, auto result) {
        try {
            if (ep)
                std::rethrow_exception(ep);

            int signal = get<1>(result);
            std::cout << "Signal: " << ::strsignal(signal) << std::endl;
            for (auto h : handles)
                if (auto s = h.lock()) {
                    // more logic could be implemented via members on a session_state struct
                    std::cout << "Shutting down live session " << s->remote_endpoint() << std::endl;
                    post(s->get_executor(), [s] { s->cancel(); });
                }
        } catch (std::exception const& e) {
            std::cout << "Server: " << e.what() << std::endl;
        }
    };

    co_spawn(io_context, listener(handles) || signals.async_wait(asio::use_awaitable), handler);

    io_context.join();
}

Online demo, and local demo:

enter image description here

sehe
  • 374,641
  • 47
  • 450
  • 633
  • Hmmm, there are solved other question. Ok, let's I asked from other side. Here (https://www.boost.org/doc/libs/1_81_0/doc/html/boost_asio/example/cpp20/coroutines/echo_server.cpp) example with coroutines. As I guess - it is single threaded. What should be changed in the example for utilize all CPUs? – AeroSun Jan 21 '23 at 12:26
  • I've updated my answer to address that in detail – sehe Jan 21 '23 at 15:35
  • thank you! It seems like what I want. But is it correctly change io_context to thread_pool ? – AeroSun Jan 21 '23 at 17:14
  • also you say that performance better in single-thread configuration if there are no significant work inside coroutine. Lets imagine web-server that during connection processing sends some local files, make some calls to databases and writes logs - in this case will async-single threaded configuration have better performance or multithreaded configuration should be used? – AeroSun Jan 21 '23 at 17:19
  • "But is it correctly change io_context to thread_pool" - it was literally the only change, what is there to ask :) – sehe Jan 21 '23 at 23:57
  • On performance: you measure. There's no such thing as "performance" anyways. You can look at latency, throughput, response-time, variance, contention, etc. [I should probably have said that in principle single-threaded can be more _efficient_ - meaning more bang for the back, less synchronization overhead.] As always, don't accept any hard/fast rule, measure. And keep an open mind about your assumptions. More threads is rarely better in IO bound things. But everything will depend on exact nature of your transactions. Perhaps you just need to pool you database transactions on a pool. – sehe Jan 22 '23 at 00:01
  • Perhaps it all doesn't matter for your case, and you can just choose the least complex option. – sehe Jan 22 '23 at 00:02