Async C++ with boost::asio

by Tyler Calabrese

April - 4 - 2024

Presentation created from Markdown using marp

Part 1: Intro

Preface: A little bit about Boost

Welcome to Boost.org! Boost provides free peer-reviewed portable C++ source libraries. (boost)

The Boost community emerged around 1998, when the first version of the standard was released. It has grown continuously since then and now plays a big role in the standardization of C++. (wikipedia)

Many modern C++ features that developers now take for granted, such as smart pointers, tuples, and even std::thread, began as Boost library features. (wikipedia)

As a full-time C++ developer, here are, in my professional opinion, the best ways to write an async
program in C++:

As a full-time C++ developer, here are, in my professional opinion, the best ways to write an async
program in C++:

  1. don't

As a full-time C++ developer, here are, in my professional opinion, the best ways to write an async
program in C++:

  1. don't
  2. boost::asio
  3. std::thread
  4. New features in C++20

Ok, then, why:

  • You’re stuck with C++
  • Mostly synchronous program with a few asynchronous needs

Example Use Cases

  1. Get performance benefits by running some tasks in parallel
  • Output: Logging, especially when you have a lot of it
  • Input: Buffer input sources, that is, load data in advance so it is ready right when you need it

Example Use Cases

2. Repeating tasks
  • Especially ones that shouldn't be blocked
  • Tell a central server that the program is still running

Example Use Cases

Notice what these all have in common:

Whether writing to a file or communicating over a network, these are all I/O operations.

Hence, boost async io. asio!

Part 2: Fundamentals

The I/O Context

  • I/O contexts are, in Boost speak, an executor: something we can submit work to.
  • Once we've given the context work to do, we run it. This is a blocking operation that will automatically
    stop once there's nothing left to do.

The I/O Context

  • The I/O context is perhaps the most confusing thing for beginners

This I/O execution context represents your program's link to the operating system's I/O services. (boost)

  • Recommendation: Wherever possible, use just one I/O context

Example: Post a single operation to an I/O context

int main()
{
    boost::asio::io_context ctx;
    boost::asio::post(ctx, [](){ std::cerr << "hi!\n"; });
    ctx.run();
}

download this code

Strands

  • We can also split other executors, called strands, off from our I/O context, and submit work to those instead.
  • Completion handlers on the same strand are guaranteed not to execute at the same time.
  • They do not guarantee anything about the order in which handlers will fire!
  • Strands become important when running an I/O context from multiple threads.

Strands

  • Recommendation: Use strands to protect your data
  • This way you don't need to mix std::atomic, std::mutex, etc. with asio

Completion Tokens

  • Boost offers many asio-compatible classes, in asio and other libraries, that can do work asynchronously.
  • Functions like this will take a completion token so we can know when the task is finished and how it went.

Completion Tokens

  • When an operation is complete,

the I/O execution context dequeues the result, translates it into an error_code, and then passes it to your completion handler. (boost)

  • We will focus on callbacks today but they also have use_future and use_awaitable.
  • Essentially, when queueing some asynchronous task, also pass in a callback that says what you want to happen next.

Part 3: asio in Action

Example: Repeat Timer

class RepeatTimer
{
public:
    explicit RepeatTimer(boost::asio::io_context& ctx, std::chrono::milliseconds every, std::function<void()> task)
    : m_timer(boost::asio::make_strand(ctx), every), m_every(every), m_task(task)
    {
        m_timer.async_wait([this](boost::system::error_code ec){ doRunOne(ec); });
    }
    ~RepeatTimer() = default; // destroying the timer cancels it, too
    
    void cancel() { m_timer.cancel(); }
    
private:
    void doRunOne(boost::system::error_code ec);
    
    boost::asio::steady_timer m_timer;
    std::chrono::milliseconds m_every;
    std::function<void()> m_task;
};

void RepeatTimer::doRunOne(boost::system::error_code ec)
{
    // we canceled or destroyed the timer
    if (ec == boost::asio::error::operation_aborted)
        return;
     // there was a problem
    else if (ec)
        throw std::runtime_error{"Repeat timer got error code " + std::to_string(ec.value())};

    // normal operation. Run our task and repeat
    m_timer.expires_after(m_every);
    m_task();
    m_timer.async_wait([this](boost::system::error_code ec){ doRunOne(ec); });
}

Example: Repeat Timer

int main()
{
    int counter = 0;
    boost::asio::io_context ctx;
    RepeatTimer t{ctx, std::chrono::seconds{1}, [&](){
        std::cerr << counter++ << std::endl;
    }};
    ctx.run_for(std::chrono::seconds{4});
}

download this code

Running an I/O Context from Multiple Threads

int main()
{
    constexpr int c_num_threads = 3;
    boost::asio::io_context ctx;

    for (int i = 0; i < c_num_threads; i++)
    {
        boost::asio::post(ctx, [](){
            std::this_thread::sleep_for(std::chrono::seconds{1});
            std::cerr << "hi!\n";
        });
    }

    boost::asio::thread_pool th{c_num_threads};
    for (int i = 0; i < c_num_threads; i++)
        boost::asio::post(th, [&ctx](){ ctx.run(); });

    th.join();

    return EXIT_SUCCESS;
}

Exercise: Let's asio-ify this code together

Exercise: Let's asio-ify this code together

(my solution)

Q&A

I promise to tell you what “asio” is short for… soon Talk about my background a bit

Talk about why not! Developers should consider _all_ the best tools for the task, including languages, when possible.

std::thread was introduced in cpp11

I'd argue that for the most part, both of these things are true at GTS :-)

Other functions besides run, such as poll, run_since, run_until, are also available.

We know we need it, but, how does boost intend us to use it? Using just one ctx enables us to get the most out of each of our threads that run it, and minimizes the starting/stopping/resetting we have to do. Not possible when different parts of the program need to be able to start/stop it at diff times

Ask questions to prepare them for the recommendation on the next slide. Can anyone figure out why strands are useful, especially in the context of OOP?

This is what really makes asio powerful!

Almost there! This is the last Big Thing we'll go over before moving on to more examples JavaScript programmers should have a basic idea of what use_future and use_awaitable are like

Take plenty of time to go over this together - initializing the timer - The error handling - canceling the timer, either with a call to cancel() or by destroying it

Thanks for your attention! What questions do people have? Can mention recruiting/career stuff now, or earlier when I talk about my background