-
Notifications
You must be signed in to change notification settings - Fork 625
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Consider allowing futures to require specialized executors? #1196
Comments
I think this was discussed recently quite a lot on the discord channel, I wonder if this would be an interesting alternative. But it was mentioned that this was experimented with but quickly became quite unergonomic, can you put some examples into your repo? |
Is there some reason you want to just have |
That was my thought too, wasn't sure if that was possible off the bat. But doesn't look too onerous |
@Nemo157 I did consider that but wasn't sure if there was enough of a use-case for alternate wake-systems. If there is, then I'd be equally happy with that proposal, since it seems like a pure generalization of this one. @jkozlowski I can try and adapt some of the |
Sorry, I don't :( I just remember that this came up recently on discord (come join us: https://discord.gg/VHe9nG), but I wasn't involved in that effort. |
@jkozlowski So far I've been able to convert
Here's the branch I'm working on, and the commit I did this in. |
Hm, actually it looks like there may be a more fundamental issue here. I don't think it's at all possible to run less specialized futures on more specialized executors specifically because it's not possible to coerce something like |
I've come here from the perspective of task-local- and timeout-able executors rustasync/team#7 (comment). These comments are, I think, what you call using static dispatch (which IMO would be the best option as it's truly 0-cost). To add to the two messages from the link above, I just noticed this would even allow libraries to be generic over eg. Basically, I believe the current design forces libraries to depend on |
I don't think this is true. You can write e.g. protocol implementations to be generic over an underlying I/O primitive, which might be tokio's TcpStream or some other executor's, and pass that primitive in during construction. Futures don't need awareness of executors to accomplish this. |
> I believe the current design forces libraries to depend on tokio as soon as they are doing non-trivial stuff
I don't think this is true. You can write e.g. protocol implementations to be generic over an underlying I/O primitive, which might be tokio's TcpStream or some other executor's, and pass that primitive in during construction. Futures don't need awareness of executors to accomplish this.
Handling network I/O without timeouts sounds like a bad idea to me, and
there is no cross-executor timeout handling (because some targets don't
support it anyway, so we can't require this from `std`).
As a consequence, futures have to hardcode *an* executor, not
necessarily tokio, to handle their timeouts.
And kind of normally, it ends up with tokio, because everyone else has
the same issue and picks tokio.
(Disclaimer: I'm hitting this issue myself, and am currently delaying
the time I'm forced to add the tokio dep by not handling timeouts for
the time being, but I won't release 0.1 until I have them handled, so…)
|
There is nothing stopping you from being generic over your timeout primitives, just like you can be generic over your I/O primitives. It would be convenient if there was a standard trait for timeout primitives, just like there are a standard traits for asynchronous byte streams, but there is absolutely nothing stopping you from defining such a trait in your library and moving on with your now-generic code. The same applies for any form of primitive asynchronous event. |
There is nothing stopping you from being generic over your timeout primitives, just like you can be generic over your I/O primitives. It would be convenient if there was a standard trait for timeout primitives, just like there are a standard traits for asynchronous byte streams, but there is absolutely nothing stopping you from defining such a trait in your library and moving on with your now-generic code. The same applies for any form of primitive asynchronous event.
Timeout primitives require support from the executor. As such, it may
require some specific context to be passed to the creation of the
timeout future. In which case some context would need to be passed to
the timeout future.
I guess currently tokio handles this with thread_locals. This is not
necessarily possible everywhere.
For instance, I may want a no_std executor that handles timeouts with a
hook in the platform clock interruptions. In which case I don't have
thread_local, and I can't do timeouts unless I'm passing something
in-band to each future.
That's why my proposition passes a `Context` that's defined by the
executor and can be refined by implementers: so that the executor could
pass in the context it knows it'll potentially need from the places
where it'll be invoked.
Having the trait in my library would make it possible to do timeouts,
but not if it requires something specific to be passed by context.
Yes, it's not strictly-speaking tokio-specific, but it's still
tokio-like-specific :)
Also, the trait-defined-in-my-lib issue is that then when one lib
depends on another, it can't blanket
```
impl<T: lib1::Trait> lib2::Trait for T
```
because of orphan rules. But that could be worked around with shared
crates whose only aim is to provide shared traits, so it's a non-issue.
|
@Ekleog I see in the other thread you suggested a design where Second, both those approaches have one limitation: the type of the context cannot depend on the lifetime of |
Again, timeouts are no different than any other primitive; sockets require support from the executor for exactly the same reasons. Hence, the same solutions apply to implementing them, without need for thread locals or universally propagated polymorphic contexts. When you pass in an executor-specific socket type to a network library, you're not just passing a file descriptor; you're also passing whatever information and handles that executor needs to bundle into the socket to process it. Similarly, when you pass in a timer factory obtained from your executor implementation, you are passing in whatever information and handles the executor deems necessary to implement timers. |
> For instance, I may want a no_std executor that handles timeouts with a hook in the platform clock interruptions. In which case I don't have thread_local, and I can't do timeouts unless I'm passing something in-band to each future.
Again, timeouts are no different than any other primitive; sockets require support from the executor for exactly the same reasons. Hence, the same solutions apply to implementing them, without need for thread locals or universally propagated polymorphic contexts. When you pass in an executor-specific socket type to a network library, you're not just passing a file descriptor; you're also passing whatever information and handles that executor needs to bundle into the socket to process it. Similarly, when you pass in a timer factory obtained from your executor implementation, you are passing in whatever information and handles the executor deems necessary to implement timers.
Passing in a timer factory is exactly what I call requiring passing
in-band arguments. It's painful for every place that wants to call it,
while being generic over the context or executor (didn't think much
about whether a future should be generic over its executor or just its
context) would just add some boilerplate when defining libraries -- and
I think we should expect libraries to do more work, not library users.
It also doesn't work for handling task-locals, because the whole point
of task-locals is that there is no in-band argument, otherwise you could
just directly pass the variables you want to put in the task-locals in
this in-band argument.
Overall, the idea I'm going with is that the ability to do timeouts,
task-locals or to open a tcp connections are properties of the executor,
and that this should be reflected in the type system if we don't want to
hit roadblocks (as we currently are for at least task-locals).
|
Actually, thinking about it again, passing arguments in-band has an additional drawback: it means it must be explicitly passed by every async/await-using function. While passing them as part of an executor-specific context (which allows lightweight executors not to support the feature and for support for it to be checked at compile-time) means that it's transparent to all functions but the ones that actually rely on the timeout / task-local / etc., it only appears in their signature as a |
@AlphaModder Good point for the lifetime. But I think poll could be defined this way, still allowing for lifetime-limited contexts, which would be useful for fn poll<'a, Ctxt: Context + 'a>(self: Pin<&'a mut Self>, cx: &'a mut Ctxt) -> Poll<...> And then On the other hand, this formulation pushes Now, if we think backwards, when does As for my reason for being generic over |
You don't need to pass a timer factory to every single async/await function any more than you need to pass a TCP socket to every single async/await function. All primitive foreign async events have the same requirements, and can be supported generically in the same way: by passing the resource you need only to the exact locations that need it.
What roadblocks? Task locals can be implemented in a straightforward fashion by hooking or wrapping an executor, as we've discussed at length. There's no blocker beyond nobody having cared enough to spec an API. |
|
I think my point is: If we transparently pass a What you propose appears to be, basically: along with every Which means either multiplying by two the number of arguments of each function that either uses a stream or forwards it to another function, or have a
There must have been discussions outside of rustasync/team#7, then, because re-reading it again doesn't help me see how to do that in practice, even considering both hooking and wrapping as possible: it'd require access to the executor at places it isn't currently accessible. Do you know where I can find those discussions? |
@AlphaModder I was thinking of using the lifetime of the |
Because other things are not necessary for the execution of every single non-degenerate future.
It is not in any way true that every single fragment of code that processes a Maybe there is some confusion on what a timer factory does. A timer factory is a tool to construct timers. For an example, see
I don't see any serious problems with the proof-of-concept approach presented on that very issue. |
Timeouts are necessary for the execution of almost every single non-degenerate futures. I don't know if you've read the SMTP spec (what I'm currently interested on), but they point out that timeouts need not be the same at every stage of the SMTP session. Meaning, basically, that you need to intersperse I must say I'm not familiar with the networking code other people write, but mine is in very much in need for it. Even though I do think the task-local is the only one that really needs fixing, because all the other ones can be emulated by stashing more stuff into task-local, however ugly that'll be forced to be if it can't be handled properly through the type system.
This proof of concept requires to wrap each future into a wrapper future before passing it to the executor. It's also the approach I adopted before the removal of A way to make this wrapping automatic is what is, as far as I understand, under discussion at rustasync/team#7. And making this automatic is precisely the thing for which I can't think of any good API, as it'd require to basically wrap the |
By my understanding IO and timers should be fully independent of "executor". An "executor" just provides the capability to run futures with an associated I believe (but still have not yet tried implementing) that it should be possible to take If you want some more complete "runtime" that is accessible alongside the wake system on the context, that could make sense. I think that should be a property of the |
The |
Forgive me if this is not the appropriate place to be discussing things like this, but has the possibility of allowing futures to specify what sort of executors they may be spawned on been examined at all? This would allow libraries to support extra features like task thread-affinity or priority.
The way I'm thinking of doing this would be to make the following changes to
Future
andContext
:Existing futures would not need to change because of the defaulted type parameter, but futures could require additional features by changing
S
to something else. For instance, if a future has a non-send subfuture one could create a new traitSpawnLocal: Spawn
which takesLocalFutureObj
instead ofFutureObj
, and implementFuture<dyn SpawnLocal>
.If anyone is interested, I have a sort of proof-of-concept for the changes in
libcore
here. (It's not an actual fork of libcore though, since I'm not quite sure how to do that.)The text was updated successfully, but these errors were encountered: