-
Notifications
You must be signed in to change notification settings - Fork 29
Consider removing spawning from futures::task::Context #56
Comments
I am also strongly against including an executor with the Task context. The argument for including it is that it always allows spawning in all future related context. However, this is not true. Instead, adding a context spawner adds yet another way to spawn, adding to the confusion. In my experience, I have rarely needed to be able to spawn from library code. The only case in which it comes up is very high level APIs (like hyper's high level API). All other libraries never spawn. Instead, they return objects that allow the caller to control how they get spawned. For example h2 does not require spawning, instead it returns a In all of my cases where spawning from a library is required, the spawner passed to the context object is insufficient. This is because it is a) a trait object and b) forces a) and b) are related. Being a trait object prevents using typed executors (executors that are designed to only execute tasks of a specific type, not Instead, tower-h2 takes an c) If the library is built to only be able to spawn with a context object, this prevents spawning from drop fns. Being able to spawn from a drop fn is necessary to be able to run async cleanup code given that drop cannot block. Because of these limitations, I do not believe the spawn argument to context will be used much at all with Tokio, instead Tokio's executor system will be the preferred method. I do not believe it to be a good idea to include a spawn argument in the type provided by |
@carllerche Thanks for this feedback. I don't have an opinion on this yet, but I find it immensely important that we discuss this properly. Here are some of related thoughts:
|
@MajorBreakfast both of those options seem to presuppose that it's worth passing an executor handle implicitly to every single future, which I don't think should be assumed, particularly given the rarity of spawning in practice. |
@Ralith Focus not so much on spawning, but on the event loop example I gave. For instance Tokio's futures require that there's an event loop around. Without a generic context the way you find out if there's an event loop around is by seeing it fail at runtime. With a generic context, there would be a trait bound that enforces that the context implements the event loop functionality. Same story for spawning. |
IMO that would be a good route to explore regardless of spawning. The needs of Tokio are very different than embedded, etc... |
I'm also in favour of removing Another argument against a generic context is actually the lack of being generic. Futures have proofed to be a difficult subject to learn and understand. Adding more complexity, via a generic context, might result in people just saying "just use Tokio's context/types" (or any other runtime). This would have the adverse effect on the genericity (is that even a word?) of Future types build by the ecosystem. |
I agree with @Thomasdezeeuw. I think we should remove the concept of |
+1-- the huge advantage here for me is that we get to remove Also, @carllerche made the excellent point to me on Discord that libraries which return an Combined with the fact that end applications will usually make use of a thread-local spawner anyways for convenience (since they're available even outside of |
FWIW, I use
Both of which options sound… unhelpful. All that to say, I think the ability to spawn from a future does make sense, and a blanket removal would be a step backwards. Then, @MajorBreakfast's solution of having some contexts implement the |
I'm not sure what that code is for (things that want to mirror |
I should have given more context to this code indeed. So basically, here there are two kinds of However, I don't want/need to write an executor, I can just run the system on any executor that supports spawning tasks. Hence my not willing to just use The solution of requiring the user to explicitly pass a spawner is a solution indeed, but makes it way less comfortable to the user, as they would have to actually pass said spawner all around their stack, which would infect just about all the code… or just use (an equivalent to) BTW, the code I'm referencing here that makes the difference between actor tasks and regular tasks is more or less a manual implementation of the |
These result in having to change pretty much the same sets of code, either you need to be generic over a If you're writing an actor system that has |
@Ekleog I'm not sure what you mean by this. However reading your next comment I think your concern is basically running the destructor when the future returned /// The public future that is returned.
pub struct Future {
/// Once `ActualFuture` returns `Poll::Ready` this will be taken (`Option::take`) and
/// `ActualFuture` will be dropped so it can run its destructor.
inner: Option<ActualFuture>,
} This, I think, would solve running your future as a task. As for running futures for the user of a library, I think most of the ecosystem will return futures for the user to run themselves. But I could be wrong here. |
@Thomasdezeeuw I think that's what he's already doing.
This seems like a problem that can be solved with documentation and diagnostics without incurring all the drawbacks of the Context API. For example, if you have task-locals, you can use them to assert that Perhaps I still don't fully understand what you're doing, but it also seems perfectly legitimate for a user to want to chain things (for example, diagnostics, or other futures that might not interact with your system at all) after your cleanup work.
It's unusual that "just about all the code" would be spawning tasks. For every application or service I've seen, spawning takes place in one or two places and everything else is composition. Is that normal for your usecase? Ultimately, this strikes me as another case of magically-propagated information being superficially convenient, but not actually letting you do any things that aren't otherwise possible, and the drawbacks are considerable. |
However, as I explore switching hyper to use 0.3, it turns out to be that I now need to propagate a I realize this is slightly off-topic to the original issue, but as it was suggested to replace the |
@MajorBreakfast Making futures generic on executor wouldn't help, because those futures are genuinely executor-independent: they are only unusual in that their wakers are invoked externally, which is something wakers already explicitly support in the general case. If we wanted to make the reactor-future dependency explicit, it would be as simple as requiring that the reactor be passed in when the I/O future is constructed, which is a decision that can be made by the implementer of the I/O future and reactor. The futures API itself is unaffected. I think this also further strengthens the case that wakers are special and are uniquely entitled to being propagated universally, whereas e.g. spawning is not. |
That's a possibility indeed, and likely what I'll do if that change passes. Unfortunately, an actor task requires locking into a global hashmap for creation, which potentially has the cost of blocking the executor, so I didn't want to force all tasks to be actors in this system. There are most likely ways around it, that said. @Ralith As @seanmonstar points out, even if you need a spawner at only one place, you need to propagate it through all the functions that lead to that place, which will soon become a problem.
Honestly, I think if I had task-locals, I wouldn't even be asking for the As for flexibility, nothing prevents a All that to say: were the proposed change coupled with the addition of |
@Ekleog I think @seanmonstar's point was more about the effort of passing a context argument through a stack of hand-written futures code. He's arguing for the removal of any sort of context entirely. It sounds like you have only one place that needs a spawner, and it's called directly by the library user, so the impact is very small in comparison. The initial discussion in this issue covers why a narrow built-in spawn function isn't a satisfactory solution at some length. |
@Ekleog IMO implementing task-locals by decorating |
@Ralith I somehow agree that a narrow spawn function isn't satisfactory, but not having any way to have a default spawner is even less satisfactory. Because the library users will have to propagate the spawner through all their code until all places where they actually call the And the end-user-simplicity point of view appears to not have been discussed until my first comment, hence my making it :) Also, it sounds like my understanding of @seanmonstar's point differs from yours, even though now I'm doubting I'm understanding correctly, so let's let them clarify. :) @carllerche I totally agree with you. However, If |
The alternative is having a standard crate like |
You could also have an external crate that implements a common interface for task locals in general. Executors that don't support it directly could be bridged into by wrapping futures or the executors themselves, as convenience dictates, all mediated by a thread-local. Not all executors would want to implement task-locals or spawning; for example, a non-allocating executor driving a fixed set of tasks probably wouldn't use either, which is fine. Since it's an external crate, you don't end up with any vestigal std interfaces needing to be stubbed out with panic. edit: Task-local discussion should probably go here: #7 |
@cramertj The problem with this solution is exactly embodied by the @Ralith I'm not convinced by the external crate solution for task-locals, as it requires a hook into the spawning process that's currently not present (as mentioned in #7's top post). Good point for moving the discussions about task-locals to #7, though IMO resolution of this issue should be blocked on resolution of #7, for the reasons stated above :) |
I don't think the current situation is any different from the situation without spawn-via-context, because neither solution provides hooks into the spawn process. Presumably solving spawn hooks can happen regardless of where spawning happens from? |
Well, the current situation allows wrapping Then, if I understand rust-lang/rust#54339 (comment) correctly, the change to remove spawning from the |
Remove spawning from task::Context r? @aturon cc rustasync/team#56
I'd just like to interject that thread local storage isn't always an option, namely in no_std environments. Whatever solution we come up with has to be able to work for no_std executors. This was the whole reason the |
@boomshroom futures 0.1 supported no_std executors by allowing them to provide custom non-thread-local storage for these objects. That is still an option now. |
The
Context
struct currently conflates two independent concerns: allowing futures to arrange to be notified viaWaker
s, and allowing new tasks to be spawned. I don't believe these belong together. Wakeup handling is useful to practically all leaf futures that aren't serviced by kernel mechanisms (i.e. that aren't leaf I/O futures), so it makes sense to ensure these facilities are passed down to every leaf. By contrast, very few futures require the ability to spawn tasks, and those that do are typically in application code (for example, the accept loop of a server) where an executor handle can be easily made available. In the rare case where library code genuinely needs to spawn new tasks, this can be easily accomplished by explicitly taking an executor handle, or by returning animpl Stream<impl Future>
whose elements can be spawned in whatever manner is appropriate to the application.The specifics of spawning a task can also vary considerably between executors in ways the generic interface exposed by
Context
cannot support. For example, applications which require non-Send
futures or which can't perform dynamic allocation cannot make use ofContext
-based spawning at all. This not only leads to awkward vestigal API surface, but also presents a subtle compatibility hazard: code using an executor that does not support spawning viaContext
will compile fine when combined with libraries that assume one, but fail at runtime when spawning is attempted. By contrast, if the ecosystem standardizes on returning streams of futures, spawning (and guarantees such asSend
ability of futures to be spawned) naturally becomes explicit.cc @carllerche, @Nemo157
The text was updated successfully, but these errors were encountered: