lewissbaker / lewissbaker.github.io

Lewis Baker's Blog

Home Page:https://lewissbaker.github.io/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Discussion for Understanding Operator co_await

lewissbaker opened this issue · comments

Please add comments here to discuss or provide feedback on https://lewissbaker.github.io/2017/11/17/understanding-operator-co-await

Very good article, thanks for sharing.

This is a great article! Thank you!

Really good article, thanks a lot. I'm excited about the next one on the promise type.

I'm co_awaiting your next installment.

commented

It looks as if coroutines were removed from the proposed standard? I can't find any reference to them on cppreference

Coroutines have not yet been adopted into the C++ draft standard, but I'm still hopeful they will be for C++20 :)

See this cppreference page for the reference to the latest version of the Coroutines TS:
https://en.cppreference.com/w/cpp/experimental

How I can compile examples?

@ddosoff You can use Compiler Explorer to compile cppcoro examples. You just need to enable the cppcoro library in the “Libraries” drop down. Alternatively check out the cppcoro repository locally and follow the build instructions in thr Readme.

Interesting to compare the amount of code (and complexity) in the C++ version of an async event and the C# one. Why does it have to be so complex (linked lists, atomic types)?

Hi Lewiss, thanks again for the great article.

Based on some examples and n4736, await_suspend could return not only void and bool but also coroutine_handle as well. When could it be useful? Also then how would compiler translation look like with it?

@insoulity the possibility to have await_suspend return coroutine_handle has been introduced recently by paper p0913. I'll suggest you read the paper (it's very short).

@iaanus Thanks for the reference. FYI one example with such an awaiter type I saw before was Nano-coroutine example by Gor's last year Cppcon.

Interesting to compare the amount of code (and complexity) in the C++ version of an async event and the C# one. Why does it have to be so complex (linked lists, atomic types)?

The C# implementation will typically make use of the Task and TaskCompletionSource classes which hide a lot of the thread synchronisation in their internals. Also, use of these classes requires heap-allocating the state for storing the result/continuation.

The implementation for C++ performs no heap allocation directly - it uses storage space from the coroutine frame and stitches these together into a linked list.

@insoulity The compiler translation for the symmetric-transfer version of await_suspend() is pretty straight forward:

// ... etc. as for other variants.
if (!awaiter.await_ready()) {
  // suspend-point
  awaiter.await_suspend(coroutine_handle<promise_type>::from_promise(promise)).resume();
  // return-to-caller-or-resumer
  // resume-point
}
awaiter.await_resume();

The really interesting thing about this variant is that the compiler is performs a guaranteed tail-call to the .resume() call on the returned coroutine_handle which allows us to do recursive coroutines without consuming stack-space.

See the latest version of cppcoro::task<T> for an example of how this can simplify and make more efficient the implementation of some coroutines/awaitables.

I've been meaning to write up more detail about the benefits of symmetric-transfer and hope to do so soon.

Hello Lewis,
Great article... In the section "Synchronisation-free async code", you mentioned that one can take advantage of await_suspend() by publishing the handle to another thread that can later resume the coroutine associated to that handle and concurrently run with await_suspend()... Is that safe to access this coroutine while another thread is also using it... I think that I need to make sure about accessing the shared data, am I wrong??? This situations cannot lead to undefined Behavior??? Please correct me if I misunderstood...

Thanks a lot for your time and articles

Should co_await be co_yield in the following para?

The Promise interface specifies methods for customising the behaviour of the coroutine itself. The library-writer is able to customise what happens when the coroutine is called, what happens when the coroutine returns (either by normal means or via an unhandled exception) and customise the behaviour of any co_await or co_yield expression within the coroutine.

Hello @lewissbaker !

Is it right that async_manual_reset_event resumes all awaiting consumers on the single thread? So, there were many awaiting threads but they will wake up on the single thread?

@subbota-a Yes, the implementation of async_manual_reset_event will resume all awaiting consumers on the thread that calls event.set() inside the call to set().

A future enhancement might require the awaiting coroutine to provide a scheduler that will be used to schedule the resumption of the coroutine at a later time rather than resuming it inline inside the call to .set(). This is the kind of thing that e.g. folly::coro::Baton does (which is similar to async_manual_reset_event).

Hello @lewissbaker,

I was wondering if this portion of the article is accurate?

{
  auto&& value = <expr>;
  auto&& awaitable = get_awaitable(promise, static_cast<decltype(value)>(value));
  auto&& awaiter = get_awaiter(static_cast<decltype(awaitable)>(awaitable));

as this would suggest that if get_awaitable and/or get_awaiter produced values, their lifetimes would be limited to the scope of this. clang 10 currently doesn't function like this, and I would like to file a bug with them, as it's very beneficial to have an awaiter's lifetime effectively hidden from the caller

@martinbonaciugome Yes, I think technically if get_awaitable() and/or get_awaiter() returns a prvalue then the lifetime of those objects will extend to the end of the full-expression, rather than their lifetimes being limited to the scope of the co_await expression.

The auto&& could be decltype(auto), although I think this is roughly equivalent due to the automatic lifetime extension when a prvalue is used to initialise a reference.

Thanks for the quick reply. That's a shame, as these objects are completely hidden and are just plumbing, yet will cause a lot more code and inefficiencies to handle coroutine destruction during suspension, and will bloat the coroutine frame substantially if co_await is used in subexpressions for long running deep coroutines like UI.

Thanks for the series. In the code skeleton in section "Awaiting the Awaiter"

Should the p in the 3 occurrences of:
handle_t::from_promise(p)
be promise as used previously in:
auto&& awaitable = get_awaitable(promise, static_cast<decltype(value)>(value));
?

Thanks

Nit typo report:

In the ASCII graph in section "Synchronisation-free async code":
<supend-point> -> <suspend-point>

Again about this section, bit of a noob question here...

auto&& value = <expr>; auto&& awaitable = get_awaitable(promise, static_cast<decltype(value)>(value)); auto&& awaiter = get_awaiter(static_cast<decltype(awaitable)>(awaitable));

What exactly is the purpose of the static_cast<decltype(awaitable)> expressions? Is it to turn possible named rvalue references back into rvalues instead of them being copied? How is this functionally different from using std::forward?

For the following section:

void async_manual_reset_event::reset() noexcept { void* oldValue = this; m_state.compare_exchange_strong(oldValue, nullptr, std::memory_order_acquire); }

Shouldn't it be std::memory_order_release? You need to release any writes that occured prior to calling reset() to any thread that just checks the status of the event with is_set which uses std::memory_order_acquire.

edit: I see in the github source you've changed reset() to use relaxed instead. This makes more sense than acquire, but I still think it should be release.

@lewissbaker

This article was published on 2017 and the spec for coroutine was released on 2020. Just wondering if this article, and other (coroutine) articles on your blog, are still conforming to the 2020 spec? 🤔

It'd be great if you put a comment/disclaimer (on the top) mentioning the status of the articles, just to avoid such confusions, or mistakenly misleading the C++ programmers (in case the articles are not conforming anymore). 😄

Hi Lewis,

  • Minor typo in Compiler <-> Library Interaction section, line There are two kinds of interfaces that are defined by the coroutines TS: The Promise interface and the Awaitable interface.: it says The Promise, instead of the Promise.
  • Obtaining the Awaiter section: why using static_cast<T&&> instead of std::foward<T> in this section's code?

Thanks!

why using static_cast<T&&> instead of std::forward<T> in this section's code?

These two are equivalent. I sometimes write static_cast<T&&> to reduce compile-times as it doesn't need to instantiate a function template. However, I think recent versions of clang now treat std::forward<T> specially so there may not be a difference any more, at least on clang.

Hi lewissbaker,

I noticed that a friend class declaration of async_manual_reset_event is missing within struct async_manual_reset_event::awaiter.

Your source on godbolt declared it though(line 49).

Hi Lewis,

Great article and series! Thanks! 🙏

A minor comment:
This can be removed from the example: friend struct awaiter; , as awaiter is a nested class.