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

Tutorial 2: going async with C++20 coroutines
PrevUpHomeNext

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

  • They aren't as versatile as async functions. For example, there is no way to set a timeout to a sync operation.
  • They don't scale well. Since sync functions block the calling thread until they complete, you need to create OS threads to achieve parallelism. This doesn't scale well and leads to the inherent complexities of multi-threaded programs.
  • Some classes (like connection_pool) only offer an async interface.

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