C++ Logo

std-proposals

Advanced search

Re: [std-proposals] Coroutines: Add explicit initialization by "co_init" operator

From: Jason McKesson <jmckesson_at_[hidden]>
Date: Wed, 9 Aug 2023 18:41:50 -0400
On Wed, Aug 9, 2023 at 6:10 PM Stefan Sichler via Std-Proposals
<std-proposals_at_[hidden]> wrote:
>
> Am 09.08.23 um 17:28 schrieb Jason McKesson via Std-Proposals:
> >> - There is currently no way to implement a well-defined initialization of the promise_type from within the Coroutine's implementation body (and pass some parameters to the promise_type constructor).
> >
> > You're not supposed to. The promise needs to exist *before* the
> > coroutine body. That means that meaningful initialization needs to
> > already have happened.
> >
>
> Hm, for me that feels like a really serious limitation that was one of
> the biggest problems for me. See the looong text in the end of the mail.
>
> (...)
>
> > Furthermore, the whole point of the coroutine system as designed is to
> > *not care* if a function is or is not a coroutine. All of that
> > machinery should be hidden, and the function body should look like
> > "normal code" as much as possible.
>
> I think we have a partly identical opinion here, but there is a
> difference: For me, the function *signature* (i.e. function declaration)
> should completely hide whether some function is a coroutine or not,
> allowing me to also implement interfaces that don't care about coroutine
> functionality under the hood.
> But I actually don't see why the function *body* (i.e. function
> definition) should hide that a function is a coroutine. As already
> pointed out, the compiler is anyway urged to inject code at the very
> beginning of the coroutine body, so why not make that explicity visile
> at that point, but instead have that secretiveness here?
>
> (...)
>
> > If you like, you can change how the coroutine's return type maps to
> > the promise type; the behavior you're citing is an overridable
> > default.
>
> I know. Please see the comment in my other mail.
>
> > However, I don't buy the notion that
> > `return_type::promise_type` being an accessible member of the return
> > type violates the notion of it being an implementation detail.
>
> I don't see the point here. If promise_type is in the return type and
> the return type is part of the coroutine signature, then it is clearly
> part of the signature which is somehow the exact opposite of an
> implementation detail.
> For example, I can't implement another coroutine with the same signatue,
> but another promise_type, which is a common problem when implementing
> interfaces, see the example below.
>
>
> >
> >> - The promise_type cannot have a member field of type of the return type, because the promise_type needs to be declared inside the return type, so the type definition of the return type is incomplete at that point.
> >
> > Why would the promise type need to have such a thing? If the two need
> > to intercommunicate, then they should do so via the return type having
> > a pointer/reference to the promise or a `coroutine_handle` or
> > something else. After all, the promise *creates* the return value
> > object; the promise has every opportunity to give it whatever it
> > needs.
>
> No, it hasn't, because it is still not initialized by the coroutine body
> at that point. So I possibly can't pass all information that I needed to
> the return value.
>
> >
> >> - The return object is created by promise_type::get_return_object() *before* the Coroutine implementation body had any chance of initializing the promise object by any means. (this was a major problem in my case that a had to work around in very ugly ways!)
> >
> > Can you explain in detail why that was a problem? Try to post code
> > where you were doing this and having a problem. I'm getting the
> > impression that you were trying to implement coroutine machinery in a
> > way that clashes with its intended design.
>
> Yes, I'm sure that the initial design didn't have my use case in mind ;-)
>
> Ok, it's an extensive framework I'm working at, but I try to describe
> the essential parts in a simplified fashion (and please see also my
> other mail):
>
> Imagine an I/O framwork, implementing I/Os on many different devices,
> requiring very different actions in the background, but implementing
> unified interfaces where possible.
> All the I/Os in the background are NOT implemented in separate threads,
> but are passed down to the hardare, which creates some kind of unified
> "IoHandle" structure (like the OVERLAPPED structure in Windows) and
> later can be checked for completion. These IoHandles are returned from
> the I/O methods.
> Regular process() calls on the IoHandle are then required to supervise
> the progress of the I/Os until completion.
>
> In such a framework, all IoHandles are of course bound to specific
> devices, so the IoHandle needs to also encapsulate a pointer to the
> device class instance the I/O was created by. This is an important point!
>
> Now, I was trying to add coroutine functionality here and there, because
> for some I/Os, it is actually required to compose them from several I/Os
> with small code snippets to be executed in between them.
> Note that this is a pretty perfect use case for coroutines (and I'm
> waiting for that functionality since many years...)!!!
>
> So what I did is adding the ability to IoHandle to optionally
> encapsulate a coroutine handle and I then implemented some I/O functions
> as coroutines returning such a handle.
> Well, that sounds pretty simple, BUT as I already pointed out, IoHandle
> is of course urged to store some pointer to the device instance.
> Even that is a problem with the current coroutine specification! Let's
> see why:
> The method signature of the I/O member method in the device class looks
> like this:
>
> virtual IoHandle do_something(blablabla...) override;
>
> When this method is implemented as a coroutine, then IoHandle is created
> by get_return_object() from the promise *before* any code in
> do_something() is executed.
> So, how should I now pass on the pointer to the device instance into the
> IoHandle???
>
> If I had a mandatory co_init operator in the method right at the
> beginning of the function body, I would be able to pass a "this" pointer
> to the initialization of the promise type which could be passed on into
> my return obejct.
>
> Do you see that point?
> I reality of course, the code is much more complex, but I tried to
> extract the essential points.
>
> ... and I'm sorry for the elongated explanation....!

As expected, this design is very confused as to how coroutines are
supposed to work. You had a design for encapsulating some asynchronous
processes and then you tried to shove coroutines into it without
adapting that design to make sense within the coroutine paradigm.
That's why you're having these problems.

So let's start with `do_something`. Within this function, you are
going to create/access some "device instance" object which represents
an asynchronous operation. You will then `co_await` on that operation
internally, scheduling part of your function's execution with that
operation. And you want to return an object so that someone else can
detect when your combined asynchronous process is finished and/or
schedule their own resumption via `co_await`.

I'm going to refer to the type of this "device instance" as
`DeviceInstance`. You say that there are many such `DeviceInstance`
classes, and they represent many different ways of implementing
similar interfaces. OK.

Here's the thing, though. It is *not* the job of your promise type nor
of your returned future type to actually implement the suspension and
resumption with `DeviceInstance`. Suspension is the compiler's job,
and resumption is the job of `DeviceInstance`, and *only* of
`DeviceInstance`. When you do `co_await device_instance_value;`,
`DeviceInstance` will be given a `coroutine_handle` to your coroutine.
Its job is to store that handle and resume it at the appropriate time
(ie: after its own async process is over).

This attachment, this relationship, does not need to have *any*
interaction with your promise. Your promise and future can check to
see if the `coroutine_handle` is `done`. They can check to see if
`return_value` or `return_void` or `unahndled_exception` have been
called.

But none of these operations requires interacting with `DeviceInstance`.

The `DeviceInstance` has your handle, but it doesn't need to have any
association with your promise/future. Your promise/future has their
handle, but they don't need to have any association with whatever
object that is currently being `co_await`ed on.

Indeed, you can `co_await` on two completely distinct "device
instance" objects, which themselves can be of distinct
`DeviceInstance` types (or any other awaitable type, for that matter).
Neither your promise nor future types should care, because neither of
them should ever know about any of those objects (there is machinery
that allows them to get involved in `co_await`, but that's for very
special case stuff, and it is not needed here).

That is how you should build your coroutine machinery.

Received on 2023-08-09 22:42:04