Implementing Simple Futures with Coroutines

Implementing Simple Futures with Coroutines

This is a cross-post from www.ModernesCpp.com.

Instead of return, a coroutine uses co_return returning its result. In this post, I want to implement a simple coroutine using co_return.

First, you may wonder why are write once more about coroutines in C++20 because I have already presented the theory behind coroutines. My answer is straightforward and based on my experience. C++20 does not provide concrete coroutines, instead, C++20 provides a framework for implementing coroutines. This framework consists of more than 20 functions, some of which you must implement, some of which you can override. Based on these functions, the compiler generates two workflows, which define the behavior of the coroutine. To make it short. Coroutines in C++20 are double-edged swords. On one side, they give you enormous power, on the other side, they are quite challenging to understand. I dedicated more than 80 pages to coroutines in my book "C++20: Get the Details", and I'm not yet explained everything.

From my experience, using simple coroutines and modifying them is the easiest - may be only - way to understand them. And this is exactly the approach I'm pursuing in the following posts. I present simple coroutines and modify them. To make the workflow obvious, I put many comments inside and add only so much theory that is necessary to understand the internals of coroutines. My explanations are by no means complete and should only serve as a starting point to deepen your knowledge about coroutines.

A Short Reminder

While you can only call a function and return from it, you can call a coroutine, suspend and resume it, and destroy a suspended coroutine.

No alt text provided for this image

With the new keywords co_await and co_yield, C++20 extends the execution of C++ functions with two new concepts.

Thanks to co_await expression it is possible to suspend and resume the execution of the expression. If you use co_await expression in a function func, the call auto getResult = func() does not block if the result of the function call func() is not available. Instead of resource-consuming blocking, you have resource-friendly waiting.

co_yield expression supports generator functions. The generator function returns a new value each time you call it. A generator function is a kind of data stream from which you can pick values. The data stream can be infinite. Therefore, we are at the center of lazy evaluation with C++.

Additionally, a coroutine does not return its result, a coroutine does co_return its result.

// ...

MyFuture<int> createFuture() {
    co_return 2021;
}

int main() {

    auto fut = createFuture();
    std::cout << "fut.get(): " << fut.get() << '\n';

}   

In this straightforward example createFuture is the coroutine because it uses one of the three new keywords co_return, co_yield, or co_await and it returns a coroutine MyFuture<int>. What? This is what often puzzled me. The name coroutine is used for two entities. Let me introduce two new terms. createFuture is a coroutine factory that returns a coroutine object fut, which can be used to ask for the result: fut.get(). 

This theory should be enough. Let's talk about co_return.

co_return

Admittedly, the coroutine in the following program eagerFuture.cpp is the simplest coroutine, I can imagine that still does something meaningful: it automatically stores the result of its invocation.

// eagerFuture.cpp

#include <coroutine>
#include <iostream>
#include <memory>

template<typename T>
struct MyFuture {
    std::shared_ptr<T> value;                           // (3)
    MyFuture(std::shared_ptr<T> p): value(p) {}
    ~MyFuture() { }
    T get() {                                          // (10)
        return *value;
    }

    struct promise_type {
        std::shared_ptr<T> ptr = std::make_shared<T>(); // (4)
        ~promise_type() { }
        MyFuture<T> get_return_object() {              // (7)
            return ptr;
        }
        void return_value(T v) {
            *ptr = v;
        }
        std::suspend_never initial_suspend() {          // (5)
            return {};
        }
        std::suspend_never final_suspend() {            // (6)
            return {};
        }
        void unhandled_exception() {
            std::exit(1);
        }
    };
};

MyFuture<int> createFuture() {                         // (1)
    co_return 2021;                                    // (9)
}

int main() {

    std::cout << '\n';

    auto fut = createFuture();
    std::cout << "fut.get(): " << fut.get() << '\n';   // (2)

    std::cout << '\n';

}

MyFuture behaves as a future, which runs immediately (see "Asynchronous Function Calls"). The call of the coroutine createFuture (line 1) returns the future, and the call fut.get (line 2) picks up the result of the associated promise.

There is one subtle difference to a future: the return value of the coroutine createFuture is available after its invocation. Due to the lifetime issues of the coroutine, the coroutine is managed by a std::shared_ptr (lines 3 and 4). The coroutine always uses std::suspend_never (lines 5, and 6) and, therefore, neither does suspend before it runs nor after. This means the coroutine is immediately executed when the function createFuture is invoked. The member function get_return_object (line 7) returns the handle to the coroutine and stores it in a local variable. return_value (lines 8) stores the result of the coroutine, which was provided by co_return 2021 (line 9). The client invokes fut.get (line 2) and uses the future as a handle to the promise. The member function get finally returns the result to the client (line 10).

No alt text provided for this image

You may think that it is not worth the effort of implementing a coroutine that behaves just like a function. You are right! However, this simple coroutine is an ideal starting point for writing various implementations of futures.

At this point, I should add a bit of theory. 

The Promise Workflow

When you use co_yield, co_await, or co_return in a function, the function becomes a coroutine, and the compiler transforms its function body to something equivalent to the following lines.

{
  Promise prom;                      // (1)
  co_await prom.initial_suspend();   // (2)
  try {                                         
    <function body>                  // (3)
  }
  catch (...) {
    prom.unhandled_exception();
  }
FinalSuspend:
  co_await prom.final_suspend();     // (4)
}

Do these function names sound familiar to you? Right! These are the member functions of the inner class promise_type. Here are the steps the compiler performs when it creates the coroutine object as the return value of the coroutine factory createFuture. It first creates the promise object (line 1), invokes its initial_suspend member function (line 2), executes the body of the coroutine factory (line 3), and finally, calls the member function final_suspend (line 4). Both member functions initial_suspend and final_suspend in the program eagerFuture.cpp return the predefined awaitables std::suspend_never. As its name suggests, this awaitable suspends never and, hence, the coroutine object suspends never und behaves such as a usual function. An awaitable is something you can await on. The operator co_await needs an awaitable. I write in a future post about the awaitable and the second awaiter workflow.

From this simplified promise workflow, you can deduce, which member functions the promise (promise_type) at least needs:

  • A default constructor
  • initial_suspend
  • final_suspend
  • unhandled_exception

Admittedly, this was not the full explanation but at least enough to get the first intuition about the workflow of coroutines.

What's next?

You may already guess it. In my next post, I use this simple coroutine as a starting point for further experiments. First, I add comments to the program to make its workflow explicit, second, I make the coroutine lazy and resume it on another thread.

 

Thanks a lot to my Patreon Supporters: Matt Braun, Roman Postanciuc, Tobias Zindl, Marko, G Prvulovic, Reinhold Dr?ge, Abernitzke, Frank Grimm, Sakib, Broeserl, António Pina, Darshan Mody, Sergey Agafyin, Андрей Бурмистров, Jake, GS, Lawton Shoemake, Animus24, Jozo Leko, John Breland, espkk, Wolfgang G?rtner, Louis St-Amour, Stephan Roslen, Venkat Nandam, Jose Francisco, Douglas Tinkham, Kuchlong Kuchlong, Avi Kohn, Robert Blanch, Truels Wissneth, Kris Kafka, Mario Luoni, Neil Wang, Friedrich Huber, lennonli, Pramod Tikare Muralidhara, Peter Ware, and Tobi Heideman.

Thanks in particular to Jon Hess, Lakshman, Christian Wittenhorst, Sherhy Pyton, Dendi Suhubdy, Sudhakar Belagurusamy, and Richard Sargeant.

My special thanks to Embarcadero

Seminars

I'm happy to give online-seminars or face-to-face seminars world-wide. Please call me if you have any questions.

Bookable (Online)

Deutsch

English

Standard Seminars 

Here is a compilation of my standard seminars. These seminars are only meant to give you a first orientation.

New

Contact Me

Modernes C++



要查看或添加评论,请登录

Rainer Grimm的更多文章

社区洞察

其他会员也浏览了