-
Notifications
You must be signed in to change notification settings - Fork 16
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
Move the descriptions of LocalWaker and Waker and the primary focus. #15
Conversation
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is fantastic -- way more clear and detailed. And the new APIs look solid!
I left a bunch of minor suggestions, mostly typographic.
Let into_waker return an Option.
Thanks for the review. I incorporated all suggestions. |
text/0000-futures.md
Outdated
which are comparable to lightweight threads. A task is a `()`-producing `Future`, | ||
which is owned by an *executor*, and polled to completion while the being pinned. | ||
|
||
*executor*s provide the ability to "spawn" such `Future`s. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was an issue with the previous text here, but anyway:
Not all executors will provide an ability to "spawn", the common minimal functionality of an executor is just to "run" futures.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought spawning one future (like block_on
does) is still spawning. But I can see how it is misleading. Updated
text/0000-futures.md
Outdated
of related definitions needed when manually implementing `Future`s or | ||
executors. | ||
Ultimately asynchronous computations are executed in the form of *tasks*, | ||
which are comparable to lightweight threads. A task is a `()`-producing `Future`, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A task is a
()
-producingFuture
This was a point of contention back when 0.3 was being implemented, the final verdict was that the future is not the task, the task is a concept on top of the future that is executor specific and not directly exposed in any APIs (indirectly the APIs under core::task
are used by the executor to communicate with the future that is at the root of the task).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, it's true. The idea was to describe that a task is some kind of running version of a Future
that is owned by the executor and accompanied by some extra state. Will try to make it clearer.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I updated the wording, and think it's better now. But I would also be fine with the one that MajorBreakfast added in
rust-lang/rust#52610
text/0000-futures.md
Outdated
defined through a `RawWaker` type, which is an struct that defines a dynamic | ||
dispatch mechanism, which consists of an raw object pointer and a virtual function | ||
pointer table (vtable). This mechanism allows implementors of an executor to | ||
customize the behavior of `RawWaker`, `Waker` and `LocalWaker` objects. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe this should be something like
This mechanism allows implementers of an executor to customize the behavior of
Waker
andLocalWaker
objects by providing an executor specificRawWaker
.
Specifically noting that executors aren't directly customizing the [Local]Waker
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sounds good. I will change it.
The API update looks good, my waker implementation definitely appears more understandable with it. |
My current opinion on this change after implementation is: The
If we see most implementations using this way, I think it should be in the same place as The next open question is that Even if yes, wouldn't it then make more sense to simply implement the Having 2 different APIs for creating |
We could only provide the way to create a |
I think this looks like an improvement on the main RFC's waker APIs. After implementing romio I've been thinking about this API a bit more carefully and I think some naming tweaks would be an improvement. First, I think it would be good for Second, I'm inclined to replace That makes the futures/reactor oriented API of wakers look like this: struct Waker { }
impl Waker {
pub fn wake(&self) { }
}
struct SyncWaker { }
impl SyncWaker {
pub fn wake(&self) { }
}
impl Into<SyncWaker> for Waker { }
impl TryInto<SyncWaker> for Waker { }
impl Send for SyncWaker { }
impl Sync for SyncWaker { } The API of future would be: trait Future {
type Output;
fn poll(self: Pin<&mut Self>, waker: &Waker) -> Poll<Self::Output>;
} The other side of this API consideration is how to handle constructing wakers, this is where things like the |
@withoutboats Thanks for your feedback! I agree that
Regarding I agree on the API being extensible regarding other waker implementations. We could later on store an The thing I'm currently mostly concerned about is |
This was actually not my experience porting tokio's reactor, which uses an
Agreed, I think we should just stick with the RawWaker type for now and leave better abstractions for the future or the external ecosystem to experiment with. |
Ok, some multithreaded future implementations totally uses
I guess there might be more of this user-code around in the end than low level library and framework implementations. So using the more concise variant there makes sense. |
I agree with the sentiment of your post though, only leaf futures and some special combinators like |
Another question I've had with this API is whether anyone is using/has plans to use I am tempted to try benchmarking the difference now though, on |
@Nemo157 I know AtomicWaker converts the localwaker to a waker internally, but the implication is that romio never uses the Waker type at all, only AtomicWaker and LocalWaker.
I would be thrilled if empirically this use case was not worth supporting because it would allow us to massively simplify the API, but @cramertj argued before that the optimization is actually really critical for single threaded executors. |
I guess this is not just any single threaded executor, but a single threaded executor coupled with a reactor in the same thread that knows it can store |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
More nits.
/// The implementation of this function must retain all resources that are | ||
/// required for this additional instance of a `RawWaker` and associated | ||
/// task. | ||
pub clone: unsafe fn(*const ()) -> RawWaker, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is an unsafe fn
pointer; but what are the invariants the function requires from its callers? It should be clearly specified in terms of "Behavior is undefined if X.". The same applies to the subsequent 3 unsafe function pointers.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Specifically, it seems to me that the requirement should be that callers must pass the data pointer of the RawWaker
that this table was created as a part of. Behavior is undefined if any other pointer is passed.
I don't see any other source of undefined behavior - aside from taking an over-broad type (*const ()
), these functions should be perfectly safe.
Co-Authored-By: Matthias247 <[email protected]>
It's actually more of an
The equivalent in C++ is Although there is another open issue from the original stabilization CR for finding a good top level module name for all of this infrastructure, and |
Right- and another thing that comes to mind now is that |
I thought a bit more about However that's not really possible with the current We could argue that |
Here is a summary of open discussion points that I gathered from this thread and the original stabilization PR:
|
/// `None`. In this case the implementation must still make | ||
/// sure that the data pointer which is passed to the function is correctly | ||
/// released, e.g. by calling the associated `drop_fn` on it. | ||
pub into_waker: unsafe fn(*const ()) -> Option<RawWaker>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this function own the data ( return *const ()
) andOption<Waker>
instead? This way if there are two different waking mechanisms for local and sync wakers it would be supported. Furthmore returning a Waker
, rather then RawWaker
, makes it clear that all the requirements of the Waker
type must be met.
Edit: first part was incorrect, as @Nemo157 pointed out.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Thomasdezeeuw if there are different implementations then the returned RawWaker
can contain different function pointers.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Nemo157 Right scratch the first part, I had in my head that data
was part of Waker
, not RawWaker
. What are you thoughts about the second part?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I could go either way. I like the fact that when writing the RawWaker
type currently you don’t need to import any of the concrete types it will be wrapped in, and think that the documentation is clear enough (and the entire area unsafe enough) that implementors will be careful about what they’re doing. But having to call Waker::new_unchecked
doesn’t seem like a massive burden (although, my current implementation of EDIT: Scratch that, I just realised I have a cast that needs to be a pointer re-reference instead which needs RawWaker
is 100% safe code, this would require adding an unsafe
block to itunsafe
).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see it the same as @Nemo157: I like the separation of Api concerns.
There should also be no extra unsafe call involved, since the new_unchecked from the result would happen in LocalWaker::into_waker()
I'd like to make a proposal building off of this: we leave the First, a core " trait Wake {
fn wake(&self);
} Second, these three construction APIs for
My understanding (which could be badly mistaken!) is that this should cover the concrete use cases today, which I believe use Arcs, Rcs, or global statics as their ownership model for wakers. We can leave the RawWaker stuff, but unstable, for people experimenting with custom shared ownership implementations. Someday we could stabilize RawWaker, or possibly a different, better factored abstraction: I've started iterating on this concept generically in an external crate called smart. Assuming this properly covers the different use cases, I think this API surface would be dramatically simpler to understand immediately than the RawWaker stuff is. |
@withoutboats AFAICT the Unless there are any uses of this API that won't work with |
@cramertj can you link to concrete implementations of waker so that I can understand them better? Perhaps the one in fuchsia? Neither of the implementations in futures (which I know are not optimal) rely on the EDIT: Its also hard to translate the code exactly, but tokio-executor's |
The |
I agree with @cramertj here. First of all we need Besides that I think I'm also not concerned about the low-levelness of |
Here are some ideas around If we split the trait into a version which is safe ( pub trait ArcWake: Send + Sync {
fn wake(arc_self: &Arc<Self>);
fn into_waker(wake: Arc<Self>) -> Waker where Self: Sized;
fn into_waker_ref(wake: &Arc<Self>) -> WakerRef<'_> where Self: Sized;
}
pub trait ArcWakeLocal: ArcWake {
unsafe fn wake_from_local(arc_self: &Arc<Self>);
unsafe fn into_waker_with_local_opt(wake: Arc<Self>) -> Waker where Self: Sized;
unsafe fn into_waker_ref_with_local_opt(wake: &Arc<Self>) -> WakerRef<'_> where Self: Sized;
} Users that want to hack together an executor in the simplest possible fashion can use Names for pub trait ArcWake: ArcSendWake { unsafe fn wake() };
pub trait ArcSendWake { fn wake_send() } in order to avoid asymetry. But implementing a That said I like the renaming from Out of those I found |
As for discussion items open, in #15 (comment) I noted that the |
@Centril I've seen the comment, but don't know exactly what you want to be added. It is described how the API should behave. And everything else is undefined behavior (which is how C-like APIs usually work). Do you have any special suggestions? I'm open for any improvements there. Feel free to directly add them to the review. |
I may be missing something, but is there a good reason why impl task::Wake for Arc<Task> {
fn wake(arc_self: &Self) {
arc_self.executor.ready_tasks.push(arc_self.clone());
arc_self.executor.notify_task_ready();
}
} Then, the implementation of Implementing `LocalWakerRef` with `Wake` taking `&self`.// This is a port of the code before the changes in this RFC, since I wanted it to be most easily
// comparable to what already exists and I'm not confident about exactly how `LocalWakerRef`
// fits into the new system.
#[derive(Debug)]
pub struct LocalWakerRef<'a> {
local_waker: LocalWaker,
_marker: PhantomData<&'a ()>,
}
impl<'a> LocalWakerRef<'a> {
pub fn new(local_waker: LocalWaker) -> Self {
LocalWakerRef {
local_waker,
_marker: PhantomData,
}
}
}
pub unsafe fn local_waker_ref<W>(wake: &W) -> LocalWakerRef<'_>
where
W: Wake + Clone /* or clone_waker on Wake */ + 'static,
{
let ptr = wake
as *const W
as *const Referenced<W>
as *const dyn UnsafeWake
as *mut dyn UnsafeWake;
let local_waker = LocalWaker::new(NonNull::new_unchecked(ptr));
LocalWakerRef::new(local_waker)
}
// Pointers to this type below are really pointers to `W`
struct Referenced<W> {
_marker: PhantomData<W>
}
unsafe impl<W: Wake + Clone /* or clone_waker on Wake */ + 'static> UnsafeWake for Referenced<W> {
#[inline]
unsafe fn clone_raw(&self) -> Waker {
let me = self as *const Referenced<W> as *const W;
W::clone(&*me).into() // or W::clone_waker(&*me)
}
#[inline]
unsafe fn drop_raw(&self) {}
#[inline]
unsafe fn wake(&self) {
let me = self as *const Referenced<W> as *const W;
W::wake(&*me)
}
#[inline]
unsafe fn wake_local(&self) {
let me = self as *const ReferencedArc<W> as *const W;
W::wake_local(&*me)
}
}
// And analogously for NonlocalReferenced<W>:
// Pointers to this type below are really pointers to `W`
struct Referenced<W> {
_marker: PhantomData<W>
}
unsafe impl<W: Wake + Clone /* or clone_waker on Wake */ + 'static> UnsafeWake for NonlocalReferenced<W> {
#[inline]
unsafe fn clone_raw(&self) -> Waker {
let me = self as *const NonlocalReferenced<W> as *const W;
W::clone(&*me).into() // or W::clone_waker(&*me)
}
#[inline]
unsafe fn drop_raw(&self) {}
#[inline]
unsafe fn wake(&self) {
let me = self as *const NonlocalReferenced<W> as *const W;
W::wake(&*me)
}
#[inline]
unsafe fn wake_local(&self) {
let me = self as *const NonlocalReferencedArc<W> as *const W;
W::wake(&*me)
}
}
#[inline]
pub fn local_waker_ref_from_nonlocal<W>(wake: &W) -> LocalWakerRef<'_>
where
W: Wake + Clone /* or clone_waker on Wake */ + 'static,
{
let ptr = wake
as *const W
as *const NonlocalReferenced<W>
as *const dyn UnsafeWake
as *mut dyn UnsafeWake;
let local_waker = unsafe { LocalWaker::new(NonNull::new_unchecked(ptr)) };
LocalWakerRef::new(local_waker)
} I not sure this would be that much better from a documentation standpoint (as no blanket
Admittedly, these could all be implemented unsafely, but it seems useful to have the safe interface be simpler, not rely on a particular fixed structure in the standard library, and be more flexible (including supporting |
/// The implementation of this function must retain all resources that are | ||
/// required for this additional instance of a `RawWaker` and associated | ||
/// task. | ||
pub clone: unsafe fn(*const ()) -> RawWaker, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Specifically, it seems to me that the requirement should be that callers must pass the data pointer of the RawWaker
that this table was created as a part of. Behavior is undefined if any other pointer is passed.
I don't see any other source of undefined behavior - aside from taking an over-broad type (*const ()
), these functions should be perfectly safe.
pub struct Waker { | ||
waker: RawWaker, | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Although it is clearly stated in the documentation, it would still probably be good to include the core difference between Waker
and LocalWaker
(the Send
and Sync
implementations) in the code snippet.
+unsafe impl Send for Waker { } | |
+unsafe impl Sync for Waker { } | |
- They must be cloneable | ||
- If all instances of a `Waker` have been dropped and their associated task had | ||
been driven to completion, all resources which had been allocated for the task | ||
must have been released. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While I agree that in practice basically every Waker
should clean up after itself, I do think calling it a requirement gives the wrong impression. This implies (to me) that a lack of resource leaks is a safety property that code can rely on for memory safety. Along the same lines that std::mem::forget
is safe but not recommended, I think that cleaning up can be mentioned, but should not be a requirement.
`std::task::Wake` trait defining wakeup behavior: | ||
If a future cannot be directly fulfilled during execution and returns `Pending`, | ||
it needs a way to later on inform the executor that it needs to get polled again | ||
to make progress. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So how do we wrap with Future
s asynchronous APIs that do not have a way to inform the execution about this without adding overhead?
This doesn't work unless you either double-allocate or force |
I may have missed this in the discussion, but its not in the RFC. I believed the intent was that an API would exist that would convert any type that implements |
@withoutboats It just works :) See this CR in the local_waker_ref file: https://github.com/rust-lang-nursery/futures-rs/pull/1340/files#diff-1ededb833a5fe62f0ee3a38329a54d6fR47 To be fair: When I implemented it I also wasn't sure whether it works, and wanted to build a generic static vtable first - which the compiler didn't accept. However it seems like static constant locals that are defined by a macro are properly accepted as static globals, which worked around this. |
@Matthias247 thanks, that makes sense. (To be specific so others dont have to look at the diff, you can use static promotion to make this work even though you can't use real statics.) |
I'm going to merge this into the main PR since it reflects the current status of the proposal. Any additional changes can be made in new PRs against the main RFC just like this was done. |
Thanks! Let's continue the discussion in the original place |
I started with adding more information around the
LocalWaker
andWaker
structs. I kept the old names since most implementations ofFutures
at the end will store the thread safe wakers, andWaker
sounded more natural thanSendWaker
for this.There are two changes on API surface level compared to the current implementation:
impl From<LocalWaker> for Waker
anymore. The reasoning was that some singlethreadedLocalWaker
s can not be converted intoWaker
s and trying to convert them leads to a panic. That seems ok an explicit method call, but not on an implicit conversion.will_wake_nonlocal/local
functions, which would need to compare twoRawWaker
s that are potentially different. I think just comparing the data pointer won't work here. Comparing data pointer and vtable exactly would work, but might not hit lots oftrue
s on executors which change the vtable. We could add another method the vtable. Or leave things out. Is there currently a use-case for these methods?I want to revisit the
Wake
section and rename things toArcWake
. This can then be a part of the RFC which can get either stabilized or not (the conversion can also live next to executor implementations). However I haven't gotten to this yet, and wanted to ask for feedback for the first parts in the meantime.