A customer wanted to have a coroutine that produced a result but kept on running. Something like this:
task<std::shared_ptr<Party>> CreateSurpriseParty(std::shared_ptr<Person> victim) { auto party = std::make_shared<Party>(); co_await party->invite_guests(); co_await party->order_food(); co_return_but_keep_running party; // hypothetical statement co_await victim->arrives(); std::cout << "Surprise!" << std::endl; }
The idea here is that somebody who asks to create a surprise party gets the Party object once the food has been ordered. However, the CreateSurpriseParty
function isn’t finished yet. It keeps on running, and then when the victim arrives, it announces a surprise.
How can you do this in a coroutine?
Well, there’s the hard way and the easy way.
The hard way is to add an “early completion” feature to the task
, so that you can write something like this:
task<std::shared_ptr<Party>> CreateSurpriseParty(std::shared_ptr<Person> victim) { auto party = std::make_shared<Party>(); co_await party->invite_guests(); co_await party->order_food(); auto producer = co_await get_result_producer(); producer.produce(party); co_await victim->arrives(); std::cout << "Surprise!" << std::endl; co_return use_already_produced_value(); }
You can use the await_transform
coroutine feature so that the result of the co_await get_result_producer()
is a special object that communicates with the promise, in this case by telling it to produce a result.
The producer object’s produce
method would do the same thing as the promise’s return_value
: It would take the argument and save it in the promise’s result holder, for await_resume
to return.
The trick is that you don’t want to wait until final_suspend
to wake the awaiting coroutine. You want to do it right away. This means that you need to update the state machine to accommodate the new scenario: The promise has resumed its awaiting coroutine, but the current coroutine is still running. One way to do this is to go back to the old pattern where we used reference counting. The produce
method would resume the awaiting coroutine, but would not decrement the reference count, because we don’t do that until we get to final_suspend
.
Another thing to do is add another overload to return_value
so that the coroutine can say “Hi, I want to finish now.” In my above example, I used a custom object called use_
as the signal.
An annoying quirk of the coroutine specification is that a promise cannot have both return_void
and return_value
methods. If that were legal, then we could say that co_return
with no arguments is how you declare that your coroutine is truly finished. (This also aligns with the principle that falling off the end of a coroutine is equivalent to co_return
with no arguments.)
But wait, we’re not done yet!
There’s a gotcha here, because a producer.produce()
that occurs while a lock is held will cause the awaiting coroutine to be resumed under the lock. That is just asking for a lock inversion deadlock. This is exactly the same problem we ran into when we tried implementing return_value
in our custom promise type, and which led to us deferring the waking of the waiting coroutine until final_suspend
.
Maybe you can code your way out of that jam, either by simply requiring that callers not hold any blocking resources when they call produce()
, or maybe changing produce()
so it resumes the awaiting coroutine immediately while transfering the current coroutine to a background thread, so the usage pattern would be
co_await producer.produce_and_resume_background(party);
But wait, we got so distracted by how we could implement this as a coroutine that we overlooked a much simpler solution. The coroutine is a red herring!
How can I write a normal (non-coroutine) function that returns a value to its caller but keeps on running?
Well, the way you would do that is to queue up the extra work to run asynchronously before returning to the caller.
std::shared_ptr<Party> CreateSurpriseParty(std::shared_ptr<Person> victim) { auto party = std::make_shared<Party>(); party->invite_guests(); party->order_food(); // arrange to be called back when the victim arrives victim.on_arrival([]() { std::cout << "Surprise!" << std::endl; }); return party; }
So just do the same thing in your coroutine.
task<std::shared_ptr<Party>>
CreateSurpriseParty(std::shared_ptr<Person> victim)
{
auto party = std::make_shared<Party>();
co_await party->invite_guests();
co_await party->order_food();
// arrange to be called back when the victim arrives
victim.on_arrival([]() {
std::cout << "Surprise!" << std::endl;
});
co_return party;
}
“Okay, Mister Smarty Pants, but what if there is no callback-based mechanism for continuing asynchronously?”
You can use a fire-and-forget coroutine.
task<std::shared_ptr<Party>> CreateSurpriseParty(std::shared_ptr<Person> victim) { auto party = std::make_shared<Party>(); co_await party->invite_guests(); co_await party->order_food(); continue_asynchronously_on_background_thread(party, victim); co_return party; } winrt::fire_and_forget continue_asynchronously_on_background_thread( std::shared_ptr<Party> party, std::shared_ptr<Person> victim) { co_await winrt::resume_background(); co_await victim.arrives(); std::cout << "Surprise!" << std::endl; }
You may be tempted to move the work inline, but be careful that your lambda doesn’t capture any variables.
Bonus reading: CppCoreGuidelines: CP.51: Do not use capturing lambdas that are coroutines.
task<std::shared_ptr<Party>>
CreateSurpriseParty(std::shared_ptr<Person> victim)
{
auto party = std::make_shared<Party>();
co_await party->invite_guests();
co_await party->order_food();
// The rest happens in parallel on a background thread.
[](auto party, auto victim) -> winrt::fire_and_forget
{
co_await winrt::resume_background();
co_await victim.arrives();
std::cout << "Surprise!" << std::endl;
}(party, victim);
co_return party;
}
I have a method that attempts to retrieve an object from a disk cache. If the object doesn't exist, I create the object but I return the object to the caller before I save it to disk.
The general C# pattern I use for this is:
<code>
The name “victim” says something.
The first thing that came to my mind is to use an asynchronous generator instead of a task. It only works if the asynchronous generator is eager, however.
Shouldn’t the bonus reading code be italicized, to indicate that it’s wrong?
The bonus reading code is not wrong: it passes party & victim as parameters instead of capturing them.