Lazy Futures with Coroutines

Contents[Show]

Based on the coroutines-based implementation of a simple future in my last post "Implementing Simple Futures with Coroutines", I want to go today one big step further. I analyze the workflow of the simple future and make it lazy.

TimelineCpp20

Before I create variations of the future, you should understand its control flow. I assume, that you know my previous post: "Implementing Simple Futures with Coroutines. In this post, comments help me to make the control flow of the coroutine transparent. Additionally, I add a link to an online compiler to each presented program so that you directly use and experiment with the programs.

The Transparent Control Flow

 

// eagerFutureWithComments.cpp

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

template<typename T>
struct MyFuture {
    std::shared_ptr<T> value
    MyFuture(std::shared_ptr<T> p): value(p) {                         // (3)
        std::cout << "    MyFuture::MyFuture" << '\n';
    }
    ~MyFuture() { 
        std::cout << "    MyFuture::~MyFuture" << '\n';
    }
    T get() {
        std::cout << "    MyFuture::get" << '\n';
        return *value;
    }

    struct promise_type {                                              // (4)
        std::shared_ptr<T> ptr = std::make_shared<T>();                // (11)
        promise_type() {
            std::cout << "        promise_type::promise_type" << '\n';
        }
        ~promise_type() { 
            std::cout << "        promise_type::~promise_type" << '\n';
        }
        MyFuture<T> get_return_object() {
            std::cout << "        promise_type::get_return_object" << '\n';
            return ptr;
        }
        void return_value(T v) {
            std::cout << "        promise_type::return_value" << '\n';
            *ptr = v;
        }
        std::suspend_never initial_suspend() {                         // (6)
            std::cout << "        promise_type::initial_suspend" << '\n';
            return {};
        }
        std::suspend_never final_suspend() noexcept {                 // (7)
            std::cout << "        promise_type::final_suspend" << '\n';
            return {};
        }
void return_void() {} void unhandled_exception() { std::exit(1); } }; // (5) }; MyFuture<int> createFuture() { // (2) std::cout << "createFuture" << '\n'; co_return 2021; } int main() { std::cout << '\n'; auto fut = createFuture(); // (1) auto res = fut.get(); // (8) std::cout << "res: " << res << '\n'; std::cout << '\n'; } // (12)

 

The call createFuture (line 1) causes the creating of the instance of MyFuture (line 2). Before MyFuture 's constructor call (line 3) is completed, the promise promise_type is created, executed, and destroyed (lines 4 - 5). The promise uses in each step of its control flow the awaitable std::suspend_never (lines 6 and 7) and, hence, never suspends. To save the result of the promise for the later fut.get() call (line 8), it has to be allocated. Furthermore, the used std::shared_ptr's ensure (lines 3 and 10) that the program does not cause a memory leak. As a local, fut goes out of scope in line 12, and the C++ run time calls its destructor.

You can try out the program on the Compiler Explorer.

eagerFutureWithComments

 

The presented coroutine runs immediately and is, therefore, eager. Furthermore, the coroutine runs in the thread of the caller.

Let's make the future lazy.

A Lazy Future

A lazy future is a future that runs only if asked for the value. Let's see what I have to change in the previous coroutine to make the future lazy.

// lazyFuture.cpp

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

template<typename T>
struct MyFuture {
    struct promise_type;
    using handle_type = std::coroutine_handle<promise_type>;
 
    handle_type coro;                                               // (5)

    MyFuture(handle_type h): coro(h) {
        std::cout << "    MyFuture::MyFuture" << '\n';
    }
    ~MyFuture() { 
        std::cout << "    MyFuture::~MyFuture" << '\n';
        if ( coro ) coro.destroy();                                 // (8)
    }

    T get() {
        std::cout << "    MyFuture::get" << '\n';
        coro.resume();                                              // (6)
        return coro.promise().result;
    }

    struct promise_type {
		T result;
        promise_type() {
           std::cout << "        promise_type::promise_type" << '\n';
        }
        ~promise_type() { 
            std::cout << "        promise_type::~promise_type" << '\n';
        }
        auto get_return_object() {                                  // (3)
            std::cout << "        promise_type::get_return_object" << '\n';
            return MyFuture{handle_type::from_promise(*this)};
        }
        void return_value(T v) {
            std::cout << "        promise_type::return_value" << '\n';
            result = v;
        }
        std::suspend_always initial_suspend() {                    // (1)
            std::cout << "        promise_type::initial_suspend" << '\n';
            return {};
        }
        std::suspend_always final_suspend() noexcept {            // (2)
            std::cout << "        promise_type::final_suspend" << '\n';
            return {};
        }
void return_void() {} void unhandled_exception() { std::exit(1); } }; }; MyFuture<int> createFuture() { std::cout << "createFuture" << '\n'; co_return 2021; } int main() { std::cout << '\n'; auto fut = createFuture(); // (4) auto res = fut.get(); // (7) std::cout << "res: " << res << '\n'; std::cout << '\n'; }

 

Let's first study the promise. The promise always suspends at the beginning (line 1) and the end (line 2). Furthermore, the member function get_return_object (line 3) creates the return object that is returned to the caller of the coroutine createFuture (line 4). The future MyFuture is more interesting. It has a handle coro (line 5) to the promise. MyFuture uses the handle to manage its promise. It resumes the promise (line 6), asks the promise for the result (line 7), and finally destroys it (line 8). The resumption of the coroutine is necessary because it never runs automatically (line 1). When the client invokes fut.get() (line 7) to ask for the result of the future, it implicitly resumes the promise (line 6).

You can try out the program on the Compiler Explorer.

lazyFuture

 

What happens if the client is not interested in the result of the future and, hence, does not resume the coroutine? Let's try it out.

int main() {

    std::cout << '\n';

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

    std::cout << '\n';

}

 

As you may guess, the promise never runs, and the member functions return_value and final_suspend are not executed.

lazyFutureWithoutGet

Before I end this post, I want to write about the lifetime challenges of coroutines.

Lifetime Challenges of Coroutines

One of the challenges of dealing with coroutines is to handle the lifetime of the coroutine.

In the first program eagerFutureWithComments.cpp, I stored the coroutine result in a std::shared_ptr. This is critical because the coroutine is eagerly executed.

In the program lazyFuture.cpp, the call final_suspend suspends always (line 2): std::suspend_always final_suspend(). Consequently, the promise outlives the client, and a std::shared_ptr is not necessary anymore. Returning std::suspend_never from the function final_suspend would cause, in this case, undefined behaviour, because the client would outlive the promise. Hence, the lifetime of the result ends bevor the client asks for it.

 

What's next?

My final step in the variation of the future is still missing. In the next post, I resume the coroutine on a separate 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, Sergey Agafyin, Андрей Бурмистров, Jake, GS, Lawton Shoemake, Animus24, Jozo Leko, John Breland, espkk, Wolfgang Gärtner, Louis St-Amour, Venkat Nandam, Jose Francisco, Douglas Tinkham, Kuchlong Kuchlong, Robert Blanch, Truels Wissneth, Kris Kafka, Mario Luoni, Neil Wang, Friedrich Huber, lennonli, Pramod Tikare Muralidhara, Peter Ware, Tobi Heideman, Daniel Hufschläger, Red Trip, Alexander Schwarz, Tornike Porchxidze, Alessandro Pezzato, and Evangelos Denaxas.

 

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

My special thanks to Embarcadero CBUIDER STUDIO FINAL ICONS 1024 Small

 

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)

German

Standard Seminars (English/German)

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

New

Contact Me

Modernes C++,

RainerGrimmSmall

 

 

Comments   

0 #1 Goalsum 2021-04-30 09:33
I tried the example in eagerFutureWithComments.cpp and in the output, MyFuture::MyFuture immediately follows promise_type::get_return_object. That's reasonable because promise_type::get_return_object calls MyFuture::MyFuture.
It seems odd that MyFuture::MyFuture happens after destruction of promise_type, which is showed in the artical's text.
Quote

My Newest E-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

Subscribe to the newsletter (+ pdf bundle)

Blog archive

Source Code

Visitors

Today 85

Yesterday 5090

Week 41531

Month 101160

All 6329632

Currently are 173 guests and no members online

Kubik-Rubik Joomla! Extensions

Latest comments