-
Notifications
You must be signed in to change notification settings - Fork 19
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
Add LocalWaker support #191
Comments
(NOT A CONTRIBUTION)
Should clarify what you mean by this: a context without a local waker set will just return its It'd be helpful probably to sketch out what the definition of
Should be possible but it may be a bit tricky.
Maybe build should just panic if no waker is set, instead of returning a Result? Not clear what anyone would do except call expect on this result. |
Yeah, currenty |
(NOT A CONTRIBUTION) possibly LocalWaker could also just be defined as |
Agreed, please outline the exact function signatures for |
Do we want to allow constructing a |
Note I proposed a
The whole purpose of |
I doubt it, contexts would be entirely dynamic with respect to their |
I'm a little confused.
You're saying you doubt it would be possible to make this a monomorphization error? Just to be clear, you're not proposing to make |
I think something like |
(NOT A CONTRIBUTION)
Via context builder, @tvallotton's proposal does make it possible to construct a Context without a waker. Calling waker on that Context would panic. The alternative is to not provide a way to construct a Context without a waker. If Rust does that, executors that don't support being awoken from other threads will just have to define a Waker that panics when you wakes it. This seems strictly worse to me.
There's no reason for this. It's always safe to wake the task from the same thread via the waker mechanism. The point of this API is that reactors that wake from the same thread that they were polled on can use the local_waker mechanism instead of waker, and they will always just work. Since these reactors often store the waker somewhere, it's important that they be able to get the same type (LocalWaker) regardless of if they are actually waking via a thread safe mechanism or not. Reactors written that way can be run on a single threaded executor or a multithreaded executor and will be totally agnostic to it. |
I was just thinking that if someone is trying to enhance performance by using local wakers, knowing if you actually got a local waker or if it just fell back to the non-local waker would be useful. |
I think it is safe to say that a 100% of the people that ask for a |
(NOT A CONTRIBUTION)
They're not the same parties. The person who wants the enhanced performance is the one who selects the executor, which isn't the same one who writes the reactor and deals with wakers, it's the one who calls |
Speaking of builders, the ability to build off of an existing I'm imagining something like this to produce the top level context: let context = ContextBuilder::new()
.top_level_poller(&mut poller)
.waker(&waker)
.local_waker(&local_waker)
.build()
.expect("at least one waker must be set"); And then any sub-future that makes its own context in order to provide its own waker (selectors and such) would inherit from the previous: let context = ContextBuilder::from(cx)
.waker(&waker)
.local_waker(&local_waker)
.build()
.expect("at least one waker must be set"); Such that an eventual leaf future could do: let poller: Option<&mut dyn TopLevelPoller> = cx.top_level_poller(); There may be other reasons to want to inherit config or interfaces from |
(NOT A CONTRIBUTION) @rust-lang/libs-api What is needed to make progress on this proposal? It seems like it has been dropped. You made the breaking change to support it last year (the actually difficult bit), but then didn't implement the API to take advantage of the change. |
From what I understand, the libs team needs to review the proposal, but they review once a week and there is a lot of proposals in the queue. Not to say that I don't share your frustration. |
Something that may be interesting to consider, is if the thread safe waker could be lazily initialized on the call to waker from a LocalWaker. I can picture a couple of ways of this could be leveraged for some optimizations. I'm thinking of the following pseudocode: thread_local! {
// storage for wakers sent to other threads
static SHARED_WAKERS: RefCell<HashMap<u32, Waker>> = RefCell::default();
}
fn create_thread_safe_waker(waker: &LocalWaker) -> Waker {
let id = new_id();
store_waker_in_thread_local(SHARED_WAKERS, id, waker);
create_waker_by_id(id)
}
let context = ContextBuilder::new()
.local_waker(local_waker)
.on_waker(create_thread_safe_waker)
.build()
.unwrap(); This way the cost of upgrading a |
I think it'd be useful to have a complete overview of the proposed API. (The LocalWaker type, the Context::local_waker method, the ContextBuilder, anything else?) |
cc @rust-lang/wg-async |
We discussed this in today's libs-api meeting. This seems like the logical next step after rust-lang/rust#95985, but we'd like to be sure that the async wg is aware of this before continuing. Thanks! |
I do have concerns about the builder API. I understand that |
I think it is quite possible we will end up moving some of the logic that is currently being handled with thread locals into context. For example, there is the scheduling budget proposal. I can also imagine some runtime agnostic way of accessing reactor utilities using context or something like that. And of course, some kind of reactor hook like the one that was mentioned by @jkarneges (which might work better if it used file descriptors instead of closures so reactors can share a thread, by the way). It could also expose executor utilities like Concerning the current proposal, we may want to offer both a let mut context = Context::from_local_waker(waker);
context.set_waker(waker); However, as it was pointed out in the IRLO thread:
|
I'm not sure if this is the correct place to comment on the API that @tvallotton has sketched up. If there's a better place, let me know. My main observation is that rather than duplicating the machinery behind Waker to create LocalWaker, there should be a clever way to re-use some of these structures. A LocalWaker is identical to a Waker in every way except for the static thread-safety guarantees. We can perhaps leverage the fact that a runtime will only ever need 1 waker. Internally, a enum WakerKind<'w> {
Local(&'w LocalWaker),
Sendable(&'w Waker)
}
impl<'w> WakerKind<'w> {
fn local_waker(&self) -> &'w LocalWaker {
match self {
Self::Local(local) => local,
Self::Sendable(sendable) => /* convert */
}
}
} At least, I can't imagine a runtime implementing both a Waker and a LocalWaker at the same time -- this feels wrong. If a runtime provides a thread-safe Waker, that Waker should suffice to be a LocalWaker as well. Additionally, there are some other ways I think the linked changes could be improved. These are relatively minor points by comparison, by still worth mentioning. One is that Rust affords us the ability to create compile time-safe builders rather than panicking. It should be possible, via trait bounds, to make Also, it may be worth considering a blanket impl If we had a redo on the API design, we would probably have that |
Could you elaborate a little further on this thought? I'm not sure if you find that there is too much duplication in
This is the case in fact, as boats said earlier:
From the runtime's perspective, they will either store a bunch of wakers in their io-reactor, or a bunch of local wakers. They won't ever need to cast a waker to a local waker themselves.
I can imagine this. For example, tokio may want to add a local waker to access a Cell instead of a Mutex when waking a task. Or a single threaded runtime may want to support both kinds of wakers, even though the runtime itself only works with
I think I've seen this sort of thing in the ecosystem be done with deprecation warnings and
I don't think this is all that bad. In fact, I think forcing users to implement |
On the note of |
WG-async discussed this in our recent triage meeting, and we plan to discuss it further later this week. |
Two ideas for possible API enhancements based on examining @tvallotton 's branch:
Both of these are additive, so they could be done later, but they'd be easy to do immediately. |
(NOT A CONTRIBUTION)
I can affirm that this is desirable. It's plausible in a single threaded executor to have a waker that has special handling for being awoken from another thread, in case another thread triggers work for a task on that executor. The LocalWaker wouldn't need to include this check, but the Waker would. They could even use the same (thread safe) data type, but with different vtables.
You don't need trait bounds for this, you can just make ContextBuilder would still have setters that overwrite these fields; just you're guaranteed LocalWaker is always set. |
I think both of @kpreid proposals seem reasonable, if no one has concerns about them I will include them in the branch.
I hadn't thought of this. It seems like an overall improvement and I can't think of any downsides. I'll include it too. |
Thinking about this a little further, I think this doesn't offer much more of what Also I made two additions to the branch that weren't discussed very much, but I think would be useful. These are:
A use case example for these two additions is the following function: use std::task::{Waker, ContextBuilder};
use std::future::{poll_fn, Future};
use std::pin::pin;
async fn with_waker<F>(f: F, waker: &Waker) -> F::Output
where
F: Future
{
let mut f = pin!(f);
poll_fn(move |cx| {
let has_waker = cx.try_waker().is_some();
if has_waker {
return f.as_mut().poll(cx);
}
let mut cx = ContextBuilder::from(cx)
.waker(&waker)
.build();
f.as_mut().poll(&mut cx)
}).await
}
with_waker(async { /* ... */ }, Waker::noop()).await; An example of a future that allows to set a Waker on Context if none was defined. |
@rust-lang/wg-async is okay with experimentation here (also, it's my -- personal, in this case -- opinion that the bar for feature-gated experimentation should be low enough as to not stifle innovation), but we would like to review the final implementation if this were to move forward to stabilization. Please keep us in the loop! |
I created a tracking issue for this feature. Among the unresolved questions I included this:
I now noticed this method exists in pub fn will_wake(&self, waker: &LocalWaker) -> bool; However, this does not allow you to know if a local waker and a waker wake the same task. I believe implementing `AsRef for waker would be the best solution for this. |
For the email users, the tracking issue @tvallotton created is rust-lang/rust#118959. |
Ok, so I implemented a new API on this other branch called lazy_waker (here is a diff). The API allows to initialize lazily a waker instead of eagerly. I think that with this API there would really be no excuse for a runtime to support The example I placed in the documentation is roughly: fn constructor(_: &LocalWaker) -> Waker {
println!("constructor was called!");
Waker::noop()
}
let mut storage = None;
let local_waker = LocalWaker::noop();
let mut cx = ContextBuilder::from_local_waker(&local_waker)
.on_waker(&mut storage, constructor)
.build(); This requires the caller to provide a storage argument for lifetime reasons. Also, while implementing this, I noticed that some of the examples break if So I believe we must figure out if we will ever need to make @withoutboats I'd be interested in what you have to say about this API. I can see some downsides, like |
You don't need a specialized API support lazily initializing the different types of wakers. When you poll the future, you can pass it a "fake" waker, and just upgrade to a real waker in the clone operation on the vtable. Note that |
Ok, after thinking this for a while I think you are correct about this. You could indeed use this to lazily initialize a waker on clone. There are a couple of edge cases, like if you send a &Waker to another thread without cloning it, but it is unlikely and almost certainly incorrect, so I'd say it is safe to abort on that case. |
We discussed this in the WG-async triage meeting today. We were still of course in favor of experimentation. We also had some high level questions we wanted to share to perhaps prompt discussion. We felt that There was also interest in the question of how this might interact with |
@traviscross I'm not really sure what is meant by suggesting the need for "LocalFuture and other LocalFoo variants." Future is not Send + Sync hence why |
I agree with @A248's comments here. Concerning this:
These futures are likely not going to be able to use |
Based on all of this discussion, however, I am starting to reconsider the current design. This discussion thread is filled with examples and patterns of how wakers, and the waking mechanism generally, are used in practice. I wonder if we might benefit by re-analyzing the motivations which led us to the current, and, following from this, if there is a cleaner solution to implement wake-up notifications in async contexts. To start with, it seems helpful to consider the futures API, not only from a runtime perspective, but also from a Future implementor's perspective. Consider what happens when a future is polled. There seem to be a few possibilities related to waking:
I've intentionally split apart 2.) and 3.), because that is the primary motivation for this issue. Also, importantly, I've decided to refer to the "waker" separately from the wake-up notification itself. The reason for this is that sending a wake-up is conceptually different from storing a waker. The waker is merely a means through which wake-up notifications are performed. After all of these, the future returns All together, a runtime provides the capabilities:
Returning to this comment I made previously:
The current approach taken in @tvallotton 's linked PR is to bifurcate the waking-related API into Waker and LocalWaker. The only difference between these is the Send + Sync thread-safety guarantee. However, I contend that this proposed design merely entrenches the conflation of multiple API use cases. Instead, delineating between thread-safe and non-thread-safe wakers might best be achieved by cleanly separating the API uses with respect to the operations I described above. These operations are waking up the current task, creating a thread-safe "waker," and creating a thread-confined "waker." Let's suppose that we model our API as having a single source of the operations described above. We could call this the A mock-up: impl Context<'_> {
/// The new API that will support all wake up related operations.
pub fn wakeup_context(&self) -> &dyn WakeupContext;
}
pub trait WakeupContext {
/// Wakes up the current task
fn wake_current_task(&self);
/// Creates a thread safe storable waker.
///
/// Panics if this operation is not supported by the runtime.
fn create_waker(&self) -> Waker;
// If fallibility is desired, then use
// fn try_create_waker(&self) -> Option<Waker>;
/// Creates a local storable waker.
fn create_local_waker(&self) -> LocalWaker;
} I think if the waker API were re-designed today with these considerations in mind, we'd end up with an API very similar to this trait. However, that doesn't mean we couldn't retrofit such a system onto the existing APIs. For compatibility, it is possible to construct a shim To explore this more fully, I created a playground for it: https://play.rust-lang.org/?version=nightly&mode=debug&edition=2021&gist=f79ed94beef7fb0cf43dbf64b38ada99 . The new design based on WakeupContext seems to better encapsulate the set of available operations. As a downside, the internal machinery is slightly more complicated to follow. Let me know if you have any feedback or if this is a workable idea. |
It's interesting to re-think things after real world experience. However, I do see one issue: there would need to be some equivalent for |
Hi @rust-lang/libs-team, @Mark-Simulacrum is asking for an explicit signoff for the currently proposed API. Do you approve the API proposed for nightly experimentation? You can view the PR here. |
Btw, I made the waker type mandatory on the last commit. Several people have expressed their concerns about wakers being optional. And while I do think that it is better for them to be optional than not, that is not something I consider terribly important for this feature to be useful. |
@rustbot labels -I-async-nominated We discussed this today in WG-async triage. We notice that the tracking issue has a checkbox for ensuring that WG-async approves the API prior to stabilization (thanks for adding that), so that will ensure that this crosses our radar again, and therefore we can unnominate this since there's probably nothing immediate for us to discuss here. Please of course nominate any questions that come up for us. |
Proposal
Problem statement
The current implementation of Waker is Send + Sync, which means that it can be freely moved between threads. While this is necessary for work stealing, it also imposes a performance penalty on single threaded and thread per core (TPC) runtimes. This runtime cost is contrary to Rust's zero cost abstraction philosophy. Furthermore, this missing API has led some async runtimes to implement unsound wakers, both by neglect and intentionally.
Motivation, use-cases
Local wakers leverage Rust's compile-time thread safety to avoid unnecessary thread synchronization, offering a compile-time guarantee that a waker has not been moved to another thread. This allows async runtimes to specialize their waker implementations and skip unnecessary atomic operations, thereby improving performance.
It is noteworthy to mention that while this is especially useful for TPC runtimes, work stealing runtimes may also benefit from performing this specialization. So this should not be considered a niche use case.
Solution sketches
Construction
Constructing a local waker is done in the same way as a shared waker. You just use the
from_raw
function, which takes aRawWaker
.Alternatively, the
LocalWake
trait may be used analogously toWake
.The safety requirements for constructing a
LocalWaker
from aRawWaker
would be the same as aWaker
, except for thread safety.Usage
A local waker can be accessed with the
local_waker
method onContext
and woken just like a regular waker. All contexts will return a validlocal_waker
, regardless of whether they are specialized for this case or not.ContextBuilder
In order to construct a specialized
Context
, theConstextBuilder
type must be used. This type may be extended in the future to allow for more functionality forContext
.Then it can be accessed with the
local_waker
method onContext
.If a
LocalWaker
is not set on a context, this one would still return a validLocalWaker
, because a local waker can be trivially constructed from the context's waker.If a runtime does not intend to support thread safe wakers, they should not provide a
Waker
toContextBuilder
, and it will construct aContext
that panics in the call towaker()
.Links and related work
What happens now?
This issue is part of the libs-api team API change proposal process. Once this issue is filed the libs-api team will review open proposals in its weekly meeting. You should receive feedback within a week or two.
The text was updated successfully, but these errors were encountered: