Boost C++ Libraries

...one of the most highly regarded and expertly designed C++ library projects in the world. Herb Sutter and Andrei Alexandrescu, C++ Coding Standards

PrevUpHomeNext

Tutorial 2: going async with C++20 coroutines

In the previous tutorial we used synchronous functions. They are simple, but have a number of limitations:

For this reason, we recommend to always use async functions. All Asio-compatible libraries (including this one) allow async programming using a variety of styles. In this chapter, we will explain how to use C++20 coroutines because they are the simplest to use.

[Note] Note

Still not using C++20? Don't worry, you can use stackful coroutines and callbacks even in C++11.

What is a coroutine?

Roughly speaking, it's a function that can suspend and resume, keeping local variables alive in the process. Suspension happens when reaching a co_await expression. These usually appear when the program performs an I/O operation. When an expression like this is encountered, the following happens:

  1. The coroutine initiates the I/O operation.
  2. The coroutine suspends, passing control back to the io_context (that is, the event loop).
  3. While the I/O operation is in progress, the io_context may run other operations, like other coroutines.
  4. When the I/O operation completes, the io_context resumes the coroutine immediately after the co_await expression.

Transforming sync code into coroutines

Recall the following code from our previous tutorial:

// The hostname, username and password to use
mysql::connect_params params;
params.server_address.emplace_host_and_port(hostname);
params.username = username;
params.password = password;

// Connect to the server
conn.connect(params);

// Issue the SQL query to the server
const char* sql = "SELECT 'Hello world!'";
mysql::results result;
conn.execute(sql, result);

// Print the first field in the first row
std::cout << result.rows().at(0).at(0) << std::endl;

// Close the connection
conn.close();

To transform this code into a coroutine, we need to:

Doing this, we have:

asio::awaitable<void> coro_main(
    mysql::any_connection& conn,
    std::string_view server_hostname,
    std::string_view username,
    std::string_view password
)
{
    // The hostname, username, password and database to use.
    // TLS is used by default.
    mysql::connect_params params;
    params.server_address.emplace_host_and_port(std::string(server_hostname));
    params.username = username;
    params.password = password;

    // Connect to the server
    co_await conn.async_connect(params);

    // Issue the SQL query to the server
    const char* sql = "SELECT 'Hello world!'";
    mysql::results result;
    co_await conn.async_execute(sql, result);

    // Print the first field in the first row
    std::cout << result.rows().at(0).at(0) << std::endl;

    // Close the connection
    co_await conn.async_close();
}

Note that the coroutine doesn't create or return explicitly any boost::asio::awaitable<void> object - this is handled by the compiler. The return type actually marks the function as being a coroutine. void here means that the coroutine doesn't return anything.

If any of the above I/O operations fail, an exception is thrown. You can prevent this by using asio::redirect_error.

Running our coroutine

As in the previous tutorial, we first need to create an io_context and a connection:

// The execution context, required to run I/O operations.
asio::io_context ctx;

// Represents a connection to the MySQL server.
mysql::any_connection conn(ctx);

To run a coroutine, use asio::co_spawn:

// Enqueue the coroutine for execution.
// This does not wait for the coroutine to finish.
asio::co_spawn(
    // The execution context where the coroutine will run
    ctx,

    // The coroutine to run. This must be a function taking no arguments
    // and returning an asio::awaitable<T>
    [&conn, argv] { return coro_main(conn, argv[3], argv[1], argv[2]); },

    // Callback to run when the coroutine completes.
    // If any exception is thrown in the coroutine body, propagate it to terminate the program.
    [](std::exception_ptr ptr) {
        if (ptr)
        {
            std::rethrow_exception(ptr);
        }
    }
);

Note that this will only schedule the coroutine. To actually run it, we need to call io_context::run. This will block the calling thread until all the scheduled coroutines and I/O operations complete:

// Calling run will actually execute the coroutine until completion
ctx.run();

Next steps

Full program listing for this tutorial is here.

You can now proceed to the next tutorial.


PrevUpHomeNext