Previous slide Next slide Toggle fullscreen Open presenter view
Async C++ with boost::asio
by Tyler Calabrese
April - 4 - 2024
Presentation created from Markdown using marp
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++:
As a full-time C++ developer, here are, in my professional opinion, the best ways to write an async
program in C++:
don't
boost::asio
std::thread
New features in C++20
Ok, then, why :
You’re stuck with C++
Mostly synchronous program with a few asynchronous needs
Example Use Cases
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 as ync io . asio!
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.
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 ;
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)
{
if (ec == boost::asio::error::operation_aborted)
return ;
else if (ec)
throw std::runtime_error{"Repeat timer got error code " + std::to_string (ec.value ())};
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;
}
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