My first coroutine

Created 27th June, 2017 11:44 (UTC), last edited 1st July, 2017 11:57 (UTC)

There are more and more examples coming out of how to convert things like the use of futures into coroutines, and you may be forgiven for thinking that there is also some magic that happens in boost::future or std::future that lets this work, but that's not the case.

In C++ a coroutine is any function that contains one of the coroutine keywords in its body, that is any of co_return, co_yield or co_await.

What we're going to do is to write a very basic mechanism that allows us to use co_return to return a value from a coroutine. Coroutines are really a generalisation of a function call, and what this is going to allow us to do is to treat a coroutine as a function call. If we can't do this then we don't stand much chance of doing anything more interesting with them, but it will give us a good starter on how the machinery works.

This version isn't going to be really useful for much, but we'll fix that in the next article.

Let's start off with a coroutine that just returns a value:

sync<int> answer() {
    std::cout << "Thinking deep thoughts..." << std::endl;
    co_return 42;
}

In order for us to be able to use answer we have to write a suitable implementation of sync that interacts in the right way with the compiler so we can do this:

int main() {
    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;
    return 0;
}

Obviously we need an implementation of sync to use here—that's what we're going to write, but first we need to understand some stuff about what's going to happen here.

When the compiler sees a coroutine it's going to take our function and split it up into parts. How these parts then get executed is what we get to control through the way that sync works.

If answer were a normal function it would be entered at the top, the body would run and the return would be used to construct the sync<int> instance. Coroutines work inside out:

  1. The coroutine is set up in a way that allows the body to be run at any time, now or later. The return value (which represents the coordination between the coroutine and its caller) needs to be created here at the start.
  2. The body gets run later on, actually at a time of our choosing.
  3. The co_return value turns up at the end of the body execution so we have to put it where the caller can get it.
  4. Was there anything else we needed?

The control for this is through a promise_type which needs to conform to a particular API. The API is somewhat fluid and the details of what it needs to look like exactly depend on the details of the machinery and the facilities that you want to be able to deal with inside the coroutine.

There's two classes involved here, promise_type and sync. The promise_type represents a part of the coroutine itself (actually part of its stack frame), and the sync handles how the coroutine interacts with its caller, and ultimately the return value.

For this first version we're only going to support co_return and not co_yield or co_await—we'll get to those later on. We also aren't actually going to do anything asynchronously to start with, we'll be happy if we can get anything at all.

So, to start with we have this (with some tracking for creation and destruction to help us see what's going on):

template<typename T>
struct sync {
    sync() {
        std::cout << "Created a sync object" << std::endl;
    }
    sync(const sync &s) {
        std::cout << "Copied a sync object" << std::endl;
    }
    ~sync() {
        std::cout << "Sync gone" << std::endl;
    }
    void get() {
        std::cout << "We got asked for the return value..." << std::endl;
    }
    struct promise_type {
        promise_type() {
            std::cout << "Promise created" << std::endl;
        }
        ~promise_type() {
            std::cout << "Promise died" << std::endl;
        }
    };
};

What step 1 above means in practice is that when the coroutine is first entered we have to remember all the stuff it needs somewhere so it'll be available no matter when the body is run. This happens by allocating a single stack frame in the heap which contains the promise_type in it together with all of the local variables that are used. Right now everything is empty so that's simple enough.

Because we could run the body of the coroutine at any time and any place, we need to make the sync instance at the start of the function call and not at the end. We tell the promise_type how to construct the sync object that the coroutine returns:

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

This just returns an empty one. The next thing we need to explain to the compiler is whether we want to start on the body straight away or not. Because we want to emulate a normal function the promise_type needs to answer “yes”:

auto initial_suspend() {
    std::cout << "Started the coroutine, don't stop now!" << std::endl;
    return std::experimental::suspend_never{};
}

This takes care of steps 1 and 2. Next we're going to hit the co_return and we'll find out what the result of our coroutine actually is. Our promise_type gets told the value, which we'll just print for now:

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

Notice that we're also answering the question of whether or not we want to suspend the coroutine after it returns a value to us. We just keep going on to step 4.

Now that we've finished the coroutine, we need to say whether we want to stop here or not. If we don't then we run out of coroutine to execute and that stack frame that was allocated in step 1 is automatically freed for us.

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

There is one last thing the compiler needs to know before it'll let us run anything. What should happen if an exception tries to escape from the coroutine? To start with we're just going to give up and bomb out:

void unhandled_exception() {
    std::exit(1);
}

Running the coroutine

With all of that done, we now have something that we can actually run.

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
Promise died
Copied a sync object
Sync gone
Got a coroutine sync object, let's get a value
We got asked for the return value…
And the coroutine value is: 0
Sync gone

This is all well and good, but we still haven't actually managed to get the return value. We do have a problem with that. Look carefully at the order of the messages.

You can see that the promise type is created first, then the return value holder (the sync instance) is created. The promise then dies and finally the sync goes. There's an extra sync to account for the one that gets copied when get_return_object returns.

The control flow is something like this:

  1. Enter main, enter the coroutine.
  2. Create the return value.
  3. Because initial_suspend returns suspend_never we enter the body of the coroutine.
  4. We hit the co_return whose value gets passed to our return_value method, which also tells the coroutine to continue executing.
  5. Because we've now finished the coroutine we hit the final_suspend and just keep going.
  6. The coroutine is now dead and its stack frame is removed, which destroys the promise_type.
  7. Execution moves back up to the call site of the coroutine which hands over the sync object returned in step 2.
  8. Finally we get the value from our sync object and display it.

You can play around with this, for example, change initial_suspend to return suspend_always.

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

You'll now see this:

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

The control flow is now this:

  1. Enter main, enter the coroutine.
  2. Create the return value.
  3. Because initial_suspend return suspend_always we return control to the call site in main.
  4. We get the sync value created in step 2.
  5. Finally we get the value from our sync object and display it.

Notice that the coroutine body never runs, it's never given an opportunity to return the real value and just as bad, the promise_type never gets destructed because we've leaked the coroutine stack frame.

In the next article we'll solve all of this properly, but for now we'll put initial_suspend back to what it was and add a std::shared_ptr to bridge the difference in lifetimes and make the correct value available.

Our complete sync class now looks like this:

template<typename T>
struct sync {
    std::shared_ptr<T> value;
    sync(std::shared_ptr<T> p)
    : value(p) {
        std::cout << "Created a sync object" << std::endl;
    }
    sync(const sync &s)
    : value(s.value) {
        std::cout << "Copied a sync object" << std::endl;
    }
    ~sync() {
        std::cout << "Sync gone" << std::endl;
    }
    T get() {
        std::cout << "We got asked for the return value..." << std::endl;
        return *value;
    }
    struct promise_type {
        std::shared_ptr<T> ptr;
        promise_type()
        : ptr(std::make_shared<T>()) {
            std::cout << "Promise created" << std::endl;
        }
        ~promise_type() {
            std::cout << "Promise died" << std::endl;
        }
        auto get_return_object() {
            std::cout << "Send back a sync" << std::endl;
            return ptr;
        }
        auto initial_suspend() {
            std::cout << "Started the coroutine, don't stop now!" << std::endl;
            return std::experimental::suspend_never{};
        }
        auto return_value(T v) {
            std::cout << "Got an answer of " << v << std::endl;
            *ptr = v;
            return std::experimental::suspend_never{};
        }
        auto final_suspend() {
            std::cout << "Finished the coro" << std::endl;
            return std::experimental::suspend_never{};
        }
        void unhandled_exception() {
            std::exit(1);
        }
    };
};

And it outputs this:

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

We got our value, and a little quirk. I guess because our sync object is now doing something more obviously useful the compiler has also worked out it can elide the copy so we now only have a single sync object.

In the next article we'll find out the proper solution to handling this lifetime issue which will get us to a fully proper working coroutine.


Categories: