Implementing Simple Futures with Coroutines

Contents[Show]

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

TimelineCpp20

You may wonder: Although I have presented the theory behind coroutines, I want to write once more about 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 and others 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 pretty challenging to understand. I dedicated over 80 pages to coroutines in my book "C++20: Get the Details", which has not yet explained everything.

From my experience, using simple coroutines and modifying them is the easiest - maybe only - way to understand them. And this is precisely 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 added only so much theory necessary to understand the internals of coroutines. My explanations are not 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.

FunctionsVersusCoroutines

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 function call's result is unavailable. 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 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, that can be used to ask for the result: fut.get()

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

 

Rainer D 6 P2 540x540Modernes C++ Mentoring

Be part of my mentoring programs:

 

 

 

 

Do you want to stay informed about my mentoring programs: Subscribe via E-Mail.

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() noexcept {  // (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).

eagerFuture

You may think it is not worth 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 into 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; hence, the coroutine object suspends never and behaves such as a usual function. An awaitable is something you can await on. The operator co_await needs an awaitable. I will write a 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 will 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, G Prvulovic, Reinhold Dröge, Abernitzke, Frank Grimm, Sakib, Broeserl, António Pina, Sergey Agafyin, Андрей Бурмистров, Jake, GS, Lawton Shoemake, Animus24, Jozo Leko, John Breland, Venkat Nandam, Jose Francisco, Douglas Tinkham, Kuchlong Kuchlong, Robert Blanch, Truels Wissneth, Kris Kafka, Mario Luoni, Friedrich Huber, lennonli, Pramod Tikare Muralidhara, Peter Ware, Daniel Hufschläger, Alessandro Pezzato, Bob Perry, Satish Vangipuram, Andi Ireland, Richard Ohnemus, Michael Dunsky, Leo Goodstadt, John Wiederhirn, Yacob Cohen-Arazi, Florian Tischler, Robin Furness, Michael Young, Holger Detering, Bernd Mühlhaus, Matthieu Bolt, Stephen Kelley, Kyle Dean, Tusar Palauri, Dmitry Farberov, Juan Dent, George Liao, Daniel Ceperley, Jon T Hess, Stephen Totten, Wolfgang Fütterer, Matthias Grün, Phillip Diekmann, Ben Atakora, Ann Shatoff, and Rob North.

 

Thanks, in particular, to Jon Hess, Lakshman, Christian Wittenhorst, Sherhy Pyton, Dendi Suhubdy, Sudhakar Belagurusamy, Richard Sargeant, Rusty Fleming, John Nebel, Mipko, Alicja Kaminska, and Slavko Radman.

 

 

My special thanks to Embarcadero CBUIDER STUDIO FINAL ICONS 1024 Small

 

My special thanks to PVS-Studio PVC Logo

 

My special thanks to Tipi.build tipi.build logo

 

My special thanks to Take Up code TakeUpCode 450 60

 

Seminars

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

Bookable (Online)

German

Standard Seminars (English/German)

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

  • C++ - The Core Language
  • C++ - The Standard Library
  • C++ - Compact
  • C++11 and C++14
  • Concurrency with Modern C++
  • Design Pattern and Architectural Pattern with C++
  • Embedded Programming with Modern C++
  • Generic Programming (Templates) with C++

New

  • Clean Code with Modern C++
  • C++20

Contact Me

Modernes C++,

RainerGrimmDunkelBlauSmall

 

 

 

Comments   

0 #1 Robert Andrzejuk 2021-03-04 15:22
Is this all executed in current thread?
Are the Caller and Function 1 thread?
Can the co-return type be such, that it cache's 10 results from co-yield?
Quote

Stay Informed about my Mentoring

 

Mentoring

English Books

Course: Modern C++ Concurrency in Practice

Course: C++ Standard Library including C++14 & C++17

Course: Embedded Programming with Modern C++

Course: Generic Programming (Templates)

Course: C++ Fundamentals for Professionals

Course: The All-in-One Guide to C++20

Course: Master Software Design Patterns and Architecture in C++

Subscribe to the newsletter (+ pdf bundle)

All tags

Blog archive

Source Code

Visitors

Today 3993

Yesterday 4371

Week 39800

Month 169925

All 12057691

Currently are 216 guests and no members online

Kubik-Rubik Joomla! Extensions

Latest comments