1

A C++20 coroutine example

 1 year ago
source link: https://mariusbancila.ro/blog/2020/06/22/a-cpp20-coroutine-example/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

A C++20 coroutine example

Posted on June 22, 2020June 24, 2020 by Marius Bancila

One of the most important new features in the C++20 is coroutines. A coroutine is a function that has the ability to be suspended and resumed. A function becomes a coroutine if it uses any of the following:

  • the co_await operator to suspend execution until resumed
  • the co_return keyword to complete execution and optionally return a value
  • the co_yield keyword to suspend execution and return a value

A coroutine must also have a return type that satisfies some requirements. However, the C++20 standard, only defines a framework for the execution of coroutines, but does not define any coroutine types satisfying such requirements. That means, we need to either write our own or rely on 3rd party libraries for this. In this post, I’ll show how to write some simple examples using the cppcoro library.

The cppcoro library contains abstractions for the C++20 coroutines, including task, generator, and async_generator. A task represents an asynchronous computation that is executed lazily (that means only when the coroutine is awaited) and a generator is a sequence of values of some T type, that are produced lazily (that is, when the begin() function is called to retrieve an iterator or the ++ operator is called on the iterator).

Let us look at an example. The function produce_items() below is a coroutine, because it uses the co_yield keyword to return a value and has the return type cppcoro::generator<std::string> that satisfies the requirements of a generator coroutine.

#include <cppcoro/generator.hpp>
cppcoro::generator<std::string> produce_items()
  while (true)
     auto v = rand();
     using namespace std::string_literals;
     auto i = "item "s + std::to_string(v);
     print_time();
     std::cout << "produced " << i << '\n';
     co_yield i;

NOTE: the use of the rand() function is for simplicity only. Do not use this obsolete function for production code.

This function has an infinite loop, but the execution is suspended when the co_yield statement executes. This function produces a random number each time it is resumed. This happens when the generator is being iterated. And example is shown below:

#include <cppcoro/task.hpp>
cppcoro::task<> consume_items(int const n)
  int i = 1;
  for(auto const& s : produce_items())
     print_time();
     std::cout << "consumed " << s << '\n';
     if (++i > n) break;
  co_return;

The consume_items function is also a coroutine. It uses the co_return keyword to complete execution and its return type is cppcodo::task<>, which also satisfies the requirements for a coroutine type. This function runs a loop n times using a range-based for loop. This loop calls the begin() function of the cppcoro::generator<std::string> class and retrieves an iterator that is later incremented with operator++. The produce_items() is resumed upon each of these calls and returns a new (random) value. If an exception occurs, it is re-throwned to the caller from the invocation of begin() or operator++. The produce_items() function could be resumed indefinitely, although the consuming code only does so for a finite number of times.

The consume_items() can be invoked from the main() function. However, because main() cannot be a coroutine, it cannot use the co_await operator to await for the completion of its execution. To help with that, the cppcoro library provides a function called sync_wait() that synchronously waits until the specified awaitable completes (which is awaited on the current thread inside a newly created coroutine). This function blocks the current thread until the operation completes and returns the result of the co_await expression. In an exception occurs, it is rethrown to the caller.

The following snipper shows how we can invoke and wait for consume_items() from main():

#include <cppcoro/sync_wait.hpp>
int main()
   cppcoro::sync_wait(consume_items(5));

The output from running this program is as follows:

time1.png

The cppcoro::generator<T> produces values in a lazy but synchronously way. That means, using the co_await operator from a coroutine returning this type is not possible. However, the cppcoro library features an asynchronous generator, called cppcoro::async_generator<T>, that makes this possible.

We can change the preceding example as follows: a new coroutine, next_value() returns a value that takes some time to be computed. We simulate that by awaiting for a random number of seconds. The produce_items() coroutine waits for a new value in each loop and then returns a new item from that value. The return type, this time, is cppcoro::async_generator<T>.

#include <cppcoro/async_generator.hpp>
cppcoro::task<int> next_value()
  using namespace std::chrono_literals;
  co_await std::chrono::seconds(1 + rand() % 5);
  co_return rand();
cppcoro::async_generator<std::string> produce_items()
  while (true)
     auto v = co_await next_value();
     using namespace std::string_literals;
     auto i = "item "s + std::to_string(v);
     print_time();
     std::cout << "produced " << i << '\n';
     co_yield i;

The consumer requires a slight change, because it has to await for each new value. This is done with the use of the co_await operator in the for loop as follows:

cppcoro::task<> consume_items(int const n)
  int i = 1;
  for co_await(auto const& s : produce_items())
     print_time();
     std::cout << "consumed " << s << '\n';
     if (++i > n) break;

The co_return statement is no longer present in this implementation, although it could be added. Because the co_await is used in the for loop, the function is coroutine. You do not need to add empty co_return statements at the end of a coroutine returning cppcoro::task<>, just like you don’t need empty return statements at the end of a regular function returning void. The previous implementation required this statement because there was no call to co_await, therefore, the co_return was necessary to make the function a coroutine.

There are no changes required to the main() function. However, when we execute the code this time, each value is produced after some random time interval, as the following image shows:

time2.png

For the sake of completeness, the print_time() function referred in these snippets is as follows:

void print_time()
   auto now = std::chrono::system_clock::now();
   std::time_t time = std::chrono::system_clock::to_time_t(now);  
   char mbstr[100];
   if (std::strftime(mbstr, sizeof(mbstr), "[%H:%M:%S] ", std::localtime(&time)))
      std::cout << mbstr;

Another important thing to note here, is that invoking co_await with a time duration is not possible out of the box. However, it is made possible by overloading the co_await operator. An implementation that works on Windows is the following:

#include <windows.h>
auto operator co_await(std::chrono::system_clock::duration duration)
   class awaiter
      static
         void CALLBACK TimerCallback(PTP_CALLBACK_INSTANCE,
            void* Context,
            PTP_TIMER)
         stdco::coroutine_handle<>::from_address(Context).resume();
      PTP_TIMER timer = nullptr;
      std::chrono::system_clock::duration duration;
   public:
      explicit awaiter(std::chrono::system_clock::duration d)
         : duration(d)
      ~awaiter()
         if (timer) CloseThreadpoolTimer(timer);
      bool await_ready() const
         return duration.count() <= 0;
      bool await_suspend(stdco::coroutine_handle<> resume_cb)
         int64_t relative_count = -duration.count();
         timer = CreateThreadpoolTimer(TimerCallback,
            resume_cb.address(),
            nullptr);
         bool success = timer != nullptr;
         SetThreadpoolTimer(timer, (PFILETIME)&relative_count, 0, 0);
         return success;
      void await_resume() {}
   return awaiter{ duration };

This implementation has been sourced from the article Coroutines in Visual Studio 2015 – Update 1.
UPDATE: The code has been changed based on the feedback. See the comments section.

To learn more about coroutines see:

Like this:

Loading...

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK