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
Explicit task::Context
argument to poll
#2
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,195 @@ | ||
- Feature Name: task-context | ||
- Start Date: 2018-01-22 | ||
- RFC PR: | ||
- Rust Issue: | ||
|
||
# Summary | ||
[summary]: #summary | ||
|
||
Learnability, reasoning, debuggability and `no_std`-compatibility improvements achieved by | ||
making task wakeup handles into explicit arguments. | ||
|
||
# Motivation | ||
[motivation]: #motivation | ||
|
||
## Learnability | ||
|
||
The `futures` task wakeup mechanism is [a regular source of confusion for new | ||
users][explicit task issue]. Looking at the signature of `Future::poll`, it's | ||
hard to tell how `Future`s work: | ||
|
||
```rust | ||
fn poll(&mut self) -> Poll<Self::Item, Self::Error>; | ||
``` | ||
|
||
`poll` takes no arguments, and returns either `Ready`, `NotReady`, or an error. | ||
From the signature, it looks as though the only way to complete a `Future` is | ||
to continually call `poll` in a loop until it returns `Ready`. This is | ||
incorrect. | ||
|
||
Furthermore, when implementing `Future`, it's necessary to schedule a wakeup | ||
when you return `NotReady`, or else you might never be `poll`ed again. The | ||
trick to the API is that there is an implicit `Task` argument to `Future::poll` | ||
which is passed using thread-local storage. This `Task` argument must be | ||
`notify`d by the `Future` in order for the task to awaken and `poll` again. | ||
It's essential to remember to do this, and to ensure that the right schedulings | ||
occur on every code path. | ||
|
||
[explicit task issue]: https://github.com/alexcrichton/futures-rs/issues/129 | ||
|
||
## Reasoning about code | ||
|
||
The implicit `Task` argument makes it difficult to tell which functions | ||
aside from `poll` will use the `Task` handle to schedule a wakeup. That has a | ||
few implications. | ||
|
||
First, it's easy to accidentally call a function that will attempt to access | ||
the task outside of a task context. Doing so will result in a panic, but it | ||
would be better to detect this mistake statically. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the only real advantage as far as I can tell. If However, today with the If you do something like |
||
|
||
Second, and relatedly, it is hard to audit a piece of code for which calls | ||
might involve scheduling a wakeup--something that is critical to get right | ||
in order to avoid "lost wakeups", which are far harder to debug than an | ||
explicit panic. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As mentioned above, you can do this by looking at the |
||
|
||
## Debuggability | ||
|
||
Today's implicit TLS task system provides us with fewer "hooks" for adding debugging | ||
tools to the task system. Deadlocks and lost wakeups are some of the trickiest things | ||
to track down with futures code today, and it's possible that by using a "two stage" | ||
system like the one proposed in this RFC, we will have more room for adding debugging | ||
hooks to track the precise points at which queing for wakeup occurred, and so on. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't see how this is true. Making the task argument explicit has no functionality change. You can hook into the task system to add debugging logic today. It is just that nobody has actually done it. |
||
|
||
More generally, the explicit `Context` argument and distinct `Waker` type provide greater | ||
scope for extensibility of the task system. | ||
|
||
## no_std | ||
|
||
While it's possible to use futures in `no_std` environments today, the approach | ||
is somewhat of a hack. Threading an explicit `&mut Context` argument makes no_std | ||
support entirely straightforward, which may also have implications for the futures | ||
library ultimately landing in libcore. | ||
|
||
# Guide-level explanation | ||
[guide-level-explanation]: #guide-level-explanation | ||
|
||
The `Future`, `Stream`, `AsyncRead`, and `AsyncWrite` traits have been modified | ||
to take explicit `task::Context` arguments. `task::Context` is a type which | ||
provides access to | ||
[task-locals](https://docs.rs/futures/*/futures/macro.task_local.html) and a `Waker`. | ||
|
||
In terms of the old `futures` task model, the `Waker` is analogous to the `Task` | ||
returned by [`task::current`][current], and the `wake` method is analogous to | ||
the old [`Task::notify`][notify]. | ||
|
||
# Reference-level explanation | ||
[reference-level-explanation]: #reference-level-explanation | ||
|
||
The `task::Context` interface looks like this: | ||
|
||
```rust | ||
mod task { | ||
|
||
/// The context for the currently running task. | ||
pub struct Context { | ||
|
||
/// Returns a `Waker` which can be used to wake up the current task. | ||
pub fn waker(&self) -> Waker { ... } | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is not Rust syntax, is There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, sorry-- There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Possible alternative design:
That way you:
|
||
|
||
/// Runs the provided closure with the task-local pointed to by the | ||
/// provided `key`. | ||
/// | ||
/// `LocalKey`s are acquired using the `task_local` macro. | ||
pub fn with_local<T, R, F>(&mut self, key: LocalKey<T>, f: F) -> R | ||
where F: FnOnce(&mut T) -> R | ||
{ | ||
... | ||
} | ||
} | ||
|
||
/// A handle used to awaken a task. | ||
pub struct Waker { | ||
|
||
/// Wakes up the associated task. | ||
/// | ||
/// This signals that the futures associated with the task are ready to | ||
/// be `poll`ed again. | ||
pub fn wake(&self) { ... } | ||
} | ||
|
||
... | ||
} | ||
``` | ||
|
||
The modified traits will look as follows: | ||
|
||
```rust | ||
pub trait Future { | ||
type Item; | ||
type Error; | ||
fn poll(&mut self, ctx: &mut Context) -> Poll<Self::Item, Self::Error>; | ||
} | ||
|
||
pub trait Stream { | ||
type Item; | ||
type Error; | ||
fn poll_next(&mut self, ctx: &mut Context) -> Poll<Option<Self::Item>, Self::Error>; | ||
} | ||
|
||
pub trait AsyncRead { | ||
unsafe fn initializer(&self) -> Initializer { ... } | ||
|
||
fn poll_read(&mut self, buf: &mut [u8], ctx: &mut Context) | ||
-> Poll<usize, io::Error>; | ||
|
||
fn poll_vectored_read(&mut self, vec: &mut [&mut IoVec], ctx: &mut Context) | ||
-> Poll<usize, io::Error> { ... } | ||
} | ||
|
||
pub trait AsyncWrite { | ||
fn poll_write(&mut self, buf: &[u8], ctx: &mut Context) | ||
-> Poll<usize, io::Error>; | ||
|
||
fn poll_vectored_write(&mut self, vec: &[&IoVec], ctx: &mut Context) | ||
-> Poll<usize, io::Error> { ... } | ||
|
||
fn poll_close(&mut self, ctx: &mut Context) -> Poll<(), io::Error>; | ||
} | ||
``` | ||
|
||
# Drawbacks | ||
[drawbacks]: #drawbacks | ||
|
||
The primary drawback here is an ergonomic hit, due to the need to explicitly thread through | ||
a `Context` argument. | ||
|
||
For combinator implementations, this hit is quite minor. | ||
|
||
For larger custom future implementations, it remains possible to use TLS internally to recover the existing ergonomics, if desired. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. IMO suggesting that people do something non-standard is a weak argument. If the non-standard strategy is better, then why is it not the standard strategy? Sticking w/ the idioms is the best option. |
||
|
||
Finally, and perhaps most importantly, the explicit argument is never seen by users who only use | ||
futures via combinators or async-await syntax, so the ergonomic impact to such users in minimal. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If the explicit argument is never seen by users who ony use futures via combinators or async-await syntax, then why not make the ergonomics solid for those of us who do? Really, it seems that this proposal hinges on:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In my experience, there's a point (and it comes pretty quickly) where you want to implement a future on you own. I'd not push that towards being an edge-case. Then again, these are mostly futures that proxy the calls through to child futures. The biggest danger there, though, is forgetting to make sure there's always a future polled, which leaves the future group in waiting for poll. The task argument doesn't fix that. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
That is a good point. The explicit task argument doesn't actually statically prevent you from making the mistake that this RFC is attempting to protect you from. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd question the notion that the TLS solution is more ergonomic. While it does reduce the amount of characters need to write a poll implementation that is at best a weak argument in favor of it. On the other hand I'd argue that keeping things explicit improves "ergonomics" just by virtue of being explicit. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Having written a lot of custom There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I assume there is something more than not needing to pass a context that makes this more ergonomic? If so, what is it? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
||
|
||
# Rationale and alternatives | ||
[alternatives]: #alternatives | ||
|
||
The two-stage design (splitting `Context` and `Waker`) is useful both for efficial task-local storage, and to provide space for debugging "hooks" in later iterations of the library. | ||
|
||
# Unresolved questions | ||
[unresolved]: #unresolved-questions | ||
|
||
- Can we come up with a better name than `task::Context`? `Task` is | ||
confusing because it suggests that we're being passed the task, | ||
when really we're _in_ the task. `TaskContext` is roughly equivalent | ||
to `task::Context` but doesn't allow users to `use futures::task::Context;` | ||
|
||
- What is the best way to integrate the explicit `task::Context` with | ||
async/await generators? If generators are changed to accept arguments to | ||
`resume`, a pointer could be passed through that interface. Otherwise, it | ||
would be necessary to pass the pointer through TLS when calling into | ||
generators. | ||
|
||
- The `task::Context::with_local` function can now likely be replaced by | ||
`HashMap`-like entry and accessor APIs. What should the precise API | ||
surface for accessing task-locals look like? |
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.
You can tell which functions will use the
Task
handle because they returnAsync
.Today, you can look at any function in the
h2
library and know if it requires a task context or not.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.
You can't tell that easily from the call site, which is the key difference in code reasoning.
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.
There are also functions like
need_read
which don't returnAsync
but do rely on an implicitTask
.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.
But you can, it's determined by the return value. Unless you mean when people rely on type inference for the return value and don't actually check that it's
Async
?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 believe I've seen code use
Async
as a "general purpose" return value to indicate readiness, without necessarily touching aTask
internally. Perhaps this practice should be discouraged if the "Async as a return value means we touched a Task" convention is to take hold.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.
But you can't easily.