'Two ways of calling coroutines nestedly?
Imagine you have those coroutines
awaitable<void> a() {
...
}
awaitable<void> b() {
co_await a();
}
awaitable<void> c() {
co_await b();
}
awaitable<void> d() {
co_await c();
}
int main() {
io_context ctx;
co_spawn(ctx, d(), detached);
ctx.run();
return 0;
}
Can I rewrite b
, c
, d
as:
awaitable<void> b() {
return a();
}
awaitable<void> c() {
return b();
}
awaitable<void> d() {
return c();
}
?
As you can see, coroutine a
does not return anything, so we can simply pass the awaitable object outside by return a()
Will you choose the first form or the second form?
Solution 1:[1]
If awaitable<void>
supports co_await
ing other awaitable<void>
(not all coroutine return objects do support co_await
ing something) than those two examples should probably conceptually do the same thing when we assume sane implementation of some "awaitable".
Will you choose the first form or the second form?
When they are conceptually equivalent (do the same thing) I would prefer second form (return a();
) as it put less stress on the compiler and does not depend on optimizations from the compiler to be performant.
As your example contains multiple layers of awaitable-returning function (which is odd) I take a liberty to talk about why this two forms are not equivalent in the general sense.
Lets pick a future
(result of asynchronous operation) as an example of some awaitable
.
Then given
future<> a() {
...
}
future<> b1() {
co_await a();
}
future<> b2() {
return a();
}
b1
and b2
should be indistinguishable to the caller, but in the general sense co_await a()
and return a()
is not equivalent. As b1
contains co_await
b1
is implemented as a coroutine and b2
is a "plain" function (as it does not contain `co_await/co_return/co_yield).
The difference between them becomes apparent if we add other code inside those functions.
future<> c1() {
// this is a coroutine (as there is co_await/co_return/co_yield inside)
// so execution of c1 body could be suspended here (see initial_suspend, eager and lazy
// coroutines)
// and next line would be executed only after resuming this coroutine once (this could
// happen on another thread or delayed in the same thread)
std::cout << "begin";
co_await a(); // this co_await could suspend execution too
// so next line could be executed only after resuming this coroutine once more (again
// it could be executed on another thread or delayed)
// and here (after `co_await`ing a future) future would be already completed and its
// result would be available (for example some side effect would be already completed)
std::cout << "after";
}
future<> c2() {
// this is a "plain" function
// next line is always executed immediately inside c2 body, before returning from c2
std::cout << "begin";
return a();
// resulting future could be still incomplete even long after returning from c2
}
future<> d1() {
// this is a coroutine as there are co_await/co_return/co_yield inside
co_await a(); // this starts and awaits result of a first a()
co_await a(); // this starts and awaits result of a second a()
// this was sequential execution of two a's
}
future<> d2() {
// this is a "plain" function
a();
return a();
// if future is eager (starts its execution even without being co_await'ed) that
// was a parallel execution of two a()
// if future is lazy (delays its execution until co_await'ed) that was
// a "future"-leak - one of the invocations of a() was discarded without executing it
// whether future is earger or lazy depends on the implementation of the future<>
// promise type (see initial_suspend)
}
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | Serikov |