Second Draft: Coroutines
Richard (Rikki) Andrew Cattermole
richard at cattermole.co.nz
Fri Jan 24 06:16:27 UTC 2025
On 24/01/2025 5:33 PM, Mai Lapyst wrote:
> On Thursday, 23 January 2025 at 23:09:42 UTC, Richard (Rikki) Andrew
> Cattermole wrote:
>>
>> On 24/01/2025 10:17 AM, Sebastiaan Koppe wrote:
>>> On Thursday, 23 January 2025 at 20:37:59 UTC, Richard (Rikki) Andrew
>>> Cattermole wrote:
>>>>
>>>> On 24/01/2025 9:12 AM, Sebastiaan Koppe wrote:
>>>>> Upon yielding a coroutine, say a socket read, you'll want to park
>>>>> the coroutine until the socket read has completed. This requires a
>>>>> signal on completion of the async operation to the execution
>>>>> context to resume the coroutine.
>>>>
>>>> Right, I handle this as part of my scheduler and worker pool.
>>>>
>>>> The language has no knowledge, nor need to know any of this which is
>>>> why it is not in the DIP.
>>>
>>> Without having a notion on how this might work I can't reasonably
>>> comment on this DIP.
>>>
>>>> How scheduling works, can only lead to confusion if it is described
>>>> in a language only proposal (I've had Walter attach on to such
>>>> descriptions in the past and was not helpful).
>>>
>>> You don't need to describe how scheduling works, just the mechanism
>>> by which a scheduler gets notified when a coroutine is ready for
>>> resumption.
>>>
>>> Rust has a Waker, C++ has the await_suspend function, etc.
>>
>> Are you wanting this snippet?
>>
>> ```d
>> // if any dependents unblock them and schedule their execution.
>> void onComplete(GenericCoroutine);
>>
>> // Depender depends upon dependency, when dependency has value or
>> completes unblock depender.
>> // May need to handle dependency for scheduling.
>> void seeDependency(GenericCoroutine dependency, GenericCoroutine
>> depender);
>>
>> // Reschedule coroutine for execution
>> void reschedule(GenericCoroutine);
>>
>> void execute(COState)(GenericCoroutine us, COState* coState) {
>> if (coState.tag >= 0) {
>> coState.execute();
>>
>> coState.waitingOnCoroutine.match{
>> (:None) {};
>>
>> (GenericCoroutine dependency) {
>> seeDependency(dependency, us);
>> };
>>
>> // Others? Future's ext.
>> };
>> }
>>
>> if (coState.tag < 0)
>> onComplete(us);
>> else
>> reschedule(us);
>> }
>> ```
>>
>> Where ``COState`` is the generated struct as per Description -> State
>> heading.
>>
>> Where ``GenericCoroutine`` is the parent struct to ``Future`` as
>> described by the DIP, that is not templated.
>>
>> Due to this depending on sumtypes I can't put it in as-is.
>>
>> Every library will do this a bit differently, but it does give the
>> general idea of it. For example you could return the dependency and
>> have it immediately executed rather than let the scheduler handle it.
>
> First off: nice work on the proposal here; I really like it. Would love
> to try it once it's in an beta stage as it's quite promising.
> As an individual that implemented their own userspace eventloop via
> fibers, I would love to have another utility in my belt to use in the
> implementation.
>
> The only thing I had a hard time figuring out what you ment by "If it
> causes an error, this error is guaranteed to be wrong in a multi-
> threaded application of it.";
> What I think is that you mean that any exception created / captured by
> the coroutine is guranteed to be indeed an execption and should be
> threaded as such.
> Correct me if I'm wrong.
Atila had a problem with this also. I haven't been able to change it as
he didn't give me anything to work from, which you did, thank you.
"If the compiler generates an error that a normal function would not
have, the error is guaranteed to not be a false positive when
considering a multithreaded context of a coroutine."
> Another thing is the visibility of the members of the created struct;
> shouldn't some of them be read-only (aka const for anyone outside) or
> completly be private?
I don't see a reason to do so (we can change this later if it is shown
to be a problem).
Its meant for library authors to have full control over lifetimes, and
inspect general lifecycle stuff.
End users should never see it.
If they can see it without explicit opting into it that is something we
should probably close a hole on.
> Like `tag`: there should be no situation where an outside entitiy should
> control the state of the coroutine, not even in as a part of a library
> or do I miss something?
You may wish to complete a coroutine early.
Nothing bad should happen if you do this.
If it does, that is likely a compiler bug, or the user did something nasty.
>> Then `yield` would be a keyword, which in turn breaks code which is
>> known to exist.
>
> Which is the same with `await`; I honestly like the way rust solved it:
> any Future (rust's equivalent to a coroutine type), has implicitly the
> `.await` method, so instead of writing `await X`, you have `X.await`.
> This dosn't break exisiting code as `.await` is still perfectly fine an
> method invocation. When we're here to reduce breaking code as much as
> possible, I strongly would go with the `.await` way instead of adding a
> new keyword.
I don't expect code breakage.
Its a new declaration so I'd be calling for this to only be available in
a new edition.
Worse case scenario we simply won't parse it in a function that isn't a
coroutine.
We have multiple tools for dealing with this :)
> For yield the only thing I can think of is to introduce a way like
> `Fiber.yield`, maybe `Coro.yield` that gets picked up by any dlang
> edition that understands coroutine and gets rewritten into a proper
> yield while older versions would see a reference to an function / field,
> which can be provided to these editions as a symbol with `static
> assert(false, "...")` to inform them about the inproper usage; but that
> would have the same problems as there could well be already such a
> construct... But if we're using an attribute, I like the `@yield` from
> Quirin's post a lot more (and `__yield` seems very clumpsy to me).
If this is needed I'm sure we can figure something out.
I'm hopeful that we'll have stuff like this figured out if changes are
needed prior to it being turned on. Although I am currently doubtful of it.
>> Rust has a Waker, ...
>> ...
>> ```
>> coState.waitingOnCoroutine.match{
>> (:None) {};
>> (GenericCoroutine dependency) {
>> seeDependency(dependency, us);
>> };
>> // Others? Future's ext.
>> };
>> ```
>
> The waker design seems much more flexible than a dependency system. For
> example, with wakers one could implement asyncronous IO by using epoll
> and invoking the waker when there's date available. I'm a bit confused
> on how that would look in your proposal. Sure your executor uses a match
> on a sumtype to determine what's it waiting on, but how does one
> "register" a custom dependency type?
Currently the DIP has no filtering on this.
It chucks the type into the sumtype (i.e. when it sees the ``await``)
and its good to go.
The library would then be responsible for going "hey I don't know what
this type is ERROR".
We may need to filter things out, which we could do once we have some
experience with it. Of course it could be possible that library code can
handle this just fine (what I expect).
> Granted, the compiler can scan the
> code and pickup any type thats been waited on as a dependency, but how
> does a executor know how to handle it? Currently, the type must be known
> beforehand from the executor, thus meaning that the executor and the IO
> library must be developed as one, instead of being two seperate things
> that only share a common protocol between them. And even when having
> compiler support for sumtypes, when the sumtype is dynamically created,
> there will be times where the sumtypes does not contain all types the
> executor can process, ending up with unreachable branches which could
> lead to compiler warnings or even errors that are cryptic.
Yes, my implementation is all in one. Eventloop + coroutine library.
This will likely need some further design work to see if we can split
them without exposing any nasty details of the coroutine library to
people who should never see it.
I don't see an issue with the sumtypes as far as usage is concerned.
```d
static if (is(Dependency : Future!ReturnType, ReturnType)) {
} else static if (is(Dependency : GenericCoroutine)) {
} else {
static assert(0, "what type is this?");
}
```
> While I agree that we should have a notion on how coroutines can be put
> to sleep until an certain event took place, I think dependencies aren't
> a great solution to that. As mentioned would a waker API be better
> suited for this task as it lets executor and IO be their own thing
> instead of trying to forcefully combine it into one.
They are not necessarily the same thing, although there are benefits in
doing so (like sharing the same thread pool).
In my library I have something called a future completion.
This is the backbone of my eventloop library for when events take place
and you want to get notification into the hands of the user like reading
from a socket (with the value that was read).
https://github.com/Project-Sidero/eventloop/blob/master/source/sidero/eventloop/coroutine/future_completion.d#L216
Essentially it allows you to use the coroutine abstraction to return a
specific value out and it works with the scheduler as if it was user
defined. Except it will never be completed by the scheduler, it is done
by some other code.
I am struggling to see how the waker/poll API from Rust is not a more
complicated mechanism for describing a dependency for when to continue.
More information about the dip.development
mailing list