A more realistic coroutine

Created 29th June, 2017 04:57 (UTC), last edited 15th July, 2017 05:39 (UTC)

Having gotten something working, we still have a small problem. We have a coroutine that we can start, and we can choose when to suspend it, but we can't yet resume it.

The coroutines TS describes a class std::experimental::coroutine_handle which is our interface to the coroutine itself. It's a template which is supposed to be told the promise_type we're using.

We can add the following to our sync class:

struct promise_type;
using handle_type = std::experimental::coroutine_handle<promise_type>;

We should store the handle in the sync object, so we can replace our default constructor with this:

sync(handle_type h)
: coro(h) {
    std::cout << "Created a sync object" << std::endl;
}
handle_type coro;

Now in our promise_type we will need to pass the correct handle when we create the sync object. The coroutine_handle has a static member that allows us to do this:

auto get_return_object() {
    std::cout << "Send back a sync" << std::endl;
    return sync<T>{handle_type::from_promise(*this)};
}

Now that we have that we should change our shared_ptr members. In the promise_type we'll replace ptr with with a plain T member called value, and we can now remove it totally from our sync object. This does mean our sync::get member needs to pull the value from the coroutine_handle, but there's an API for that:

T get() {
    std::cout << "We got asked for the return value..." << std::endl;
    return coro.promise().value;
}

We clearly also have to change the way we record the return value in the promise_type:

auto return_value(T v) {
    std::cout << "Got an answer of " << v << std::endl;
    value = v;
    return std::experimental::suspend_never{};
}

If we try to run this we'll find we still have a problem with the lifetimes. The sync instance still outlives our promise_type instance so we hit undefined behaviour. The lifetime issue can be solved by suspending before we run off the end of the coroutine:

auto final_suspend() {
    std::cout << "Finished the coro" << std::endl;
    return std::experimental::suspend_always{};
}

I've rather arbitrarily changed the final_suspend member here, but you could also change return_value. Just so long as the value in the promise_type gets filled in before we suspend we'll be fine.

Now it leaks again of course, but we can plug the leak by destroying the coroutine through its handle in the sync destructor:

~sync() {
    std::cout << "Sync gone" << std::endl;
    if ( coro ) coro.destroy();
}

Now everything lines up and it all works, although there is still a subtle problem. If we copy the sync object around then we're going to end up destroying the coroutine_handle from more than one place.

When we deal with a coroutine_handle we have to remember that this is a resource and needs to be managed properly. The simplest way to deal with this is to make our sync movable but not copyable.

sync(const sync &) = delete;
sync(sync &&s)
: coro(s.coro) {
    std::cout << "Sync moved leaving behind a husk" << std::endl;
    s.coro = nullptr;
}
~sync() {
    std::cout << "Sync gone" << std::endl;
    if ( coro ) coro.destroy();
}
sync &operator = (const sync &) = delete;
sync &operator = (sync &&s) {
    coro = s.coro;
    s.coro = nullptr;
    return *this;
}

This final version deals correctly with all lifetime issues and outputs something along these lines:

Promise created
Send back a sync
Created a sync object
Started the coroutine, don't stop now!
Thinking deep thoughts…
Got an answer of 42
Finished the coro
Sync gone
Got a coroutine, let's get a value
We got asked for the return value…
And the coroutine value is: 42
Sync gone
Promise died

A lazy coroutine

We now have a working coroutine that executes its body when it's entered. Let's instead see if we can make a lazy coroutine that doesn't execute its body until it gets asked for the value.

Our general strategy is going to be this:

  1. Start the coroutine
  2. Return the lazy instance that will co-ordinate the return value.
  3. Suspend the coroutine.
  4. When the value in the lazy object is asked for, resume the coroutine.
  5. Suspend the coroutine after the co_return.
  6. The lazy object will return the value.
  7. Destroy the coroutine when the lazy is destructed.

There aren't actually that many changes we need to make. First of all just duplicate the sync class and call it lazy—we'll refactor this at the end.

Just to check nothing boneheaded happened first alter answer to return a lazy<int> rather than the sync and make sure we observe the same behaviour.

In order to delay the execution of the coroutine body we need to return suspend_always from initial_suspend:

auto initial_suspend() {
    std::cout << "Started the coroutine, put the brakes on!" << std::endl;
    return std::experimental::suspend_always{};
}

Now when we run it the coroutine body is never run so we don't see the proper value.

Promise created
Send back a lazy
Created a lazy object
Started the coroutine, put the brakes on!
Lazy gone
Got a coroutine, let's get a value
We got asked for the return value…
And the coroutine value is: 0
Lazy gone
Promise died

Luckily the coroutine_handle has a member that allows us to resume the execution of the coroutine at a time of our choosing.

T get() {
    std::cout << "We got asked for the return value..." << std::endl;
    coro.resume();
    return coro.promise().value;
}

With this additional change we now get the right result again, and we can see that things happen when we wanted them to:

Promise created
Send back a lazy
Created a lazy object
Started the coroutine, put the brakes on!
Lazy gone
Got a coroutine, let's get a value
We got asked for the return value…
Thinking deep thoughts…
Got an answer of 42
Finished the coro
And the coroutine value is: 42
Lazy gone
Promise died

Control the return type, control the coroutine

This describes the general pattern when working with coroutines. The way that the coroutine works in detail is fully described by the way that its return type controls the coroutine machinery provided the language. This has the unfortunate side effect of rendering useless auto type deduction for the return types. Oh well.

Notice how in both our sync and lazy examples the body of answer not changed at all. In both cases the literal 42 is returned (an int), but we wrap in something that controls how that value finds it way to the caller of answer.

Now that we understand that, we can refactor our code to remove the redundancy and highlight the important differences between the two implementations.

With that done it's easy to see that the promise types for two differ in whether they suspend or not at the initial suspend point, they also (obviously) differ in the types of object returned by get_return_object. And the final place they differ is in the get implementation which for lazy must also resume the coroutine:

template<typename T>
struct sync : public coreturn<T> {
    using coreturn<T>::coreturn;
    using handle_type = typename coreturn<T>::handle_type;
    T get() {
        std::cout << "We got asked for the return value..." << std::endl;
        return coreturn<T>::get();
    }
    struct promise_type : public coreturn<T>::promise {
        auto get_return_object() {
            std::cout << "Send back a sync" << std::endl;
            return sync<T>{handle_type::from_promise(*this)};
        }
        auto initial_suspend() {
            std::cout << "Started the coroutine, don't stop now!" << std::endl;
            return std::experimental::suspend_never{};
        }
    };
};


template<typename T>
struct lazy : public coreturn<T> {
    using coreturn<T>::coreturn;
    using handle_type = typename coreturn<T>::handle_type;;
    T get() {
        std::cout << "We got asked for the return value..." << std::endl;
        this->coro.resume();
        return coreturn<T>::get();
    }
    struct promise_type : public coreturn<T>::promise {
        auto get_return_object() {
            std::cout << "Send back a lazy" << std::endl;
            return lazy<T>{handle_type::from_promise(*this)};
        }
        auto initial_suspend() {
            std::cout << "Started the coroutine, put the brakes on!" << std::endl;
            return std::experimental::suspend_always{};
        }
    };
};

The common part is now:

template<typename T>
struct coreturn {
    struct promise;
    friend struct promise;
    using handle_type = std::experimental::coroutine_handle<promise>;
    coreturn(const coreturn &) = delete;
    coreturn(coreturn &&s)
    : coro(s.coro) {
        std::cout << "Coreturn wrapper moved" << std::endl;
        s.coro = nullptr;
    }
    ~coreturn() {
        std::cout << "Coreturn wrapper gone" << std::endl;
        if ( coro ) coro.destroy();
    }
    coreturn &operator = (const coreturn &) = delete;
    coreturn &operator = (coreturn &&s) {
        coro = s.coro;
        s.coro = nullptr;
        return *this;
    }
    struct promise {
        friend struct coreturn;
        promise() {
            std::cout << "Promise created" << std::endl;
        }
        ~promise() {
            std::cout << "Promise died" << std::endl;
        }
        auto return_value(T v) {
            std::cout << "Got an answer of " << v << std::endl;
            value = v;
            return std::experimental::suspend_never{};
        }
        auto final_suspend() {
            std::cout << "Finished the coro" << std::endl;
            return std::experimental::suspend_always{};
        }
        void unhandled_exception() {
            std::exit(1);
        }
    private:
        T value;
    };
protected:
    T get() {
        return coro.promise().value;
    }
    coreturn(handle_type h)
    : coro(h) {
        std::cout << "Created a coreturn wrapper object" << std::endl;
    }
    handle_type coro;
};

A final problem

There is one final thing we need to think about. What happens if we fetch the answer from the coroutine more than once? We can change main like this:

int main() {
    std::cout << "coro1" << std::endl;
    auto a = answer();
    std::cout << "Got a coroutine, let's get a value" << std::endl;
    auto v = a.get();
    std::cout << "And the coroutine value is: " << v << std::endl;
    v = a.get();
    std::cout << "And the coroutine value is still: " << v << std::endl;
    return 0;
}

On my machine I get this output:

Promise created
Send back a lazy
Created a coreturn wrapper object
Started the coroutine, put the brakes on!
Coreturn wrapper moved
Coreturn wrapper gone
Got a coroutine, let's get a value
We got asked for the return value…
Thinking deep thoughts…
Got an answer of 42
Finished the coro
And the coroutine value is: 42
We got asked for the return value…

And then it crashes. What happens of course is that we resume a coroutine which is already at its final suspension point, which means we run off the end of it and it's automatically destroyed for us. This ends badly.

What we need to do is make sure we only resume the coroutine the first time get is called. Luckily there is a simple way to do this:

T get() {
    std::cout << "We got asked for the return value..." << std::endl;
    if ( not this->coro.done() ) this->coro.resume();
    return coreturn<T>::get();
}

done will return true only when the coroutine is at its final suspension point (the one after the body is exited). With that change we have a nice reliable lazy function that will only ever be evaluated once.


Next time we're going to extend this further and start to look at the co_yield mechanism which will allow us to build some generators.


Categories: