This repository has been archived by the owner on Jun 21, 2019. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 3
Tokio reform #1
Closed
Closed
Tokio reform #1
Changes from 2 commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,326 @@ | ||
# Summary | ||
[summary]: #summary | ||
|
||
Simplify the Tokio project by renaming the `tokio-core` crate to `tokio`, | ||
greatly reducing ergonomic issues in the existing `tokio-core` API, and | ||
de-emphasize `tokio-proto` and `tokio-service` in the online documentation. | ||
|
||
# Motivation | ||
[motivation]: #motivation | ||
|
||
Perhaps the largest roadblock to Tokio's adoption today is its steep learning | ||
curve, an opinion shared by a large number of folks! The number one motivation | ||
of this RFC is to tackle this problem head-on, ensuring that the technical | ||
foundation itself of Tokio is simplified to enable a much smoother introductory | ||
experience into the "world of async" in Rust. | ||
|
||
One mistake we made early on in the Tokio project was to so promiently mention | ||
and seemingly recommend the `tokio-proto` and `tokio-service` crates in the | ||
documentation. The `tokio-proto` crate itself is only intended to be used by | ||
authors implementing protocols, which is in theory a pretty small number of | ||
people! Instead though the implementation and workings of `tokio-proto` threw | ||
many newcomers for a spin as they struggled to understand how `tokio-proto` | ||
solved their problem. It's our intention that with this RFC the `tokio-proto` | ||
and `tokio-service` crates are effectively deprecated. This will provide a | ||
strong signal that users and newcomers should be entering at a different point | ||
of the stack. | ||
|
||
Anecdotally we've had more success with the `tokio-core` crate being easier to | ||
pick up and not as complicated, but it's not without its own problems. The | ||
distinction between `Core`, `Handle`, and `Remote` is subtle and can be | ||
difficult to grasp, for example. Furthermore we've since clarified that | ||
`tokio-core` is conflating two different concerns in the same crate: spawning | ||
tasks and managing I/O. Our hope is to rearchitect the `tokio-core` crate with a | ||
drastically simpler API surface area to make introductory examples easier to | ||
read and libraries using `tokio-core` easier to write. | ||
|
||
Finally one of the main points of confusion around the Tokio project has been | ||
around the many layers that are available to you. Each crate under the Tokio | ||
umbrella provides a slightly different entry point and is intended for different | ||
audiences, but it's often difficult to understand what audience you're supposed | ||
to be in! This in turn has led us to the conclusion that we'd like to rename the | ||
`tokio-core` crate under the new name `tokio`. This crate, `tokio`, will be the | ||
heart soul of the Tokio project, deferring frameworks like the functionality | ||
`tokio-proto` and `tokio-service` provide to different libraries and projects. | ||
|
||
It is our intention that after this reorganization happens the introduction to | ||
the Tokio project is a much more direct and smoother path than it is today. | ||
There will be fewer crates to consider (just `tokio`) which have a much smaller | ||
API to work with (detailed below) and should allow us to tackle the heart of | ||
async programming, futures, much more quickly in the documentation. | ||
|
||
# Guide-level explanation | ||
[guide-level-explanation]: #guide-level-explanation | ||
|
||
The Tokio project, intended to be the foundation of the asynchronous I/O | ||
ecosystem in Rust, is defined by its main crate, `tokio`. The `tokio` crate will | ||
provide an implementation of an event loop, powered by the cross-platform `mio` | ||
library. The main feature of `tokio` is to enable using I/O objects to implement | ||
futures, such as TCP connections, UDP sockets, etc. | ||
|
||
The `tokio` crate by default has a global event loop that all I/O will be | ||
processed on. The global event loop enables std-like servers to be created, for | ||
example this would be an echo server written with `tokio`: | ||
|
||
```rust | ||
extern crate futures; | ||
extern crate tokio; | ||
extern crate tokio_io; | ||
|
||
use std::env; | ||
use std::net::SocketAddr; | ||
|
||
use futures::{Stream, Future}; | ||
use futures::unsync::CurrentThread; | ||
use tokio::net::TcpListener; | ||
use tokio::reactor::Handle; | ||
use tokio_io::AsyncRead; | ||
use tokio_io::io::copy; | ||
|
||
fn main() { | ||
let addr = env::args().nth(1).unwrap_or("127.0.0.1:8080".to_string()); | ||
let addr = addr.parse::<SocketAddr>().unwrap(); | ||
|
||
// Notice that unlike today, no `handle` argument is needed when creating a | ||
// `TcpListener`. | ||
let handle = Handle::global(); | ||
let socket = TcpListener::bind(&addr, handle).unwrap(); | ||
println!("Listening on: {}", addr); | ||
|
||
let done = socket.incoming().for_each(move |(socket, addr)| { | ||
let (reader, writer) = socket.split(); | ||
let amt = copy(reader, writer); | ||
let msg = amt.then(move |result| { | ||
match result { | ||
Ok((amt, _, _)) => println!("wrote {} bytes to {}", amt, addr), | ||
Err(e) => println!("error on {}: {}", addr, e), | ||
} | ||
|
||
Ok(()) | ||
}); | ||
|
||
// Again, unlike today you don't need a `handle` to spawn but can | ||
// instead spawn futures conveniently through the `CurrentThread` type | ||
// in the `futures` crate | ||
CurrentThread.spawn(msg); | ||
Ok(()) | ||
}); | ||
|
||
done.wait().unwrap(); | ||
} | ||
``` | ||
|
||
The purpose of the global event loop is to free users by default from having to | ||
worry about what an event loop is or how to interact with it. Instead most | ||
servers "will just work" as I/O objects, timeouts, etc, all get bound to the | ||
global event loop. | ||
|
||
### Spawning in `futures` | ||
|
||
The `futures` crate will grow a type named `CurrentThread` which is an | ||
implementation of the `Executor` trait for spawning futures onto the current | ||
thread. This type serves the ability to spawn a future to run "concurrently in | ||
the background" when the thread is otherwise blocked on other futures-related | ||
tasks. For example while calling `wait` all futures will continue to make | ||
progress. | ||
|
||
One important change with this is that the ability to "spawn onto a `Core`" is | ||
no longer exposed, and this will need to be constructed manually if desired. | ||
|
||
# Reference-level explanation | ||
[reference-level-explanation]: #reference-level-explanation | ||
|
||
## The `tokio` crate API | ||
|
||
The new `tokio` crate will have a similar API to `tokio-core`, but with a few | ||
notable differences: | ||
|
||
* `Core` is now both `Send` and `Sync`, but methods continue to take `&mut self` | ||
for `poll` and `turn` to ensure exclusive access when running a `Core`. This | ||
restriction may also be lifted in the future. | ||
* The `Handle` type is now also both `Send` and `Sync`. This removes the need | ||
for `Remote`. The `Handle` type also has a `global` method to acquire a | ||
handle to the global event loop. | ||
* All spawning related functionality is removed in favor of implementations in | ||
the `futures` crate. | ||
|
||
The removal of the distinction between `Handle` and `Remote` is made possible | ||
through removing the ability to spawn. This means that a `Core` itself is | ||
fundamentally `Send`-compatible and with a tweak to the implementation we can | ||
get both `Core` and `Handle` to be both `Send` and `Sync`. | ||
|
||
All methods will continue to take `&Handle` but it's not required to create a | ||
`Core` to acquire a `Handle`. Instead the `Handle::global` function can be used | ||
to extract a handle to the global even tloop. | ||
|
||
The remaining APIs of `Core` are the `run` and `turn` methods. Although `Core` | ||
is `Send` and `Sync` it's not intended to rationalize the behavior of concurrent | ||
usage of `Core::run` just yet. Instead both of these methods continue to take | ||
`&mut self`. It's expected that in the future we'll rationalize a story for | ||
concurrently calling these methods, but that's left to a future RFC. | ||
|
||
## Event loop management | ||
|
||
It is intended that all application architectures using `tokio-core` today will | ||
continue to be possible with `tokio` tomorrow. By default, however, a lazily | ||
initialized global event loop will be executed on a helper thread for each | ||
process. Applications can continue, if necessary, to create and run a `Core` | ||
manually to avoid usage of the helper thread and its `Core`. | ||
|
||
|
||
Code that currently looks like this will continue to work: | ||
|
||
```rust | ||
let mut core = Core::new().unwrap(); | ||
let handle = core.handle(); | ||
let listener = TcpListener::bind(&addr, &handle).unwrap(); | ||
let server = listener.incoming().for_each(/* ... */); | ||
core.run(server).unwrap(); | ||
``` | ||
|
||
although examples and documentation will instead recommend a pattern that looks | ||
like: | ||
|
||
```rust | ||
let handle = Handle::global(); | ||
let listener = TcpListener::bind(&addr, handle).unwrap(); | ||
let server = listener.incoming().for_each(/* ... */); | ||
server.wait().unwrap(); | ||
``` | ||
|
||
## Spawning Futures | ||
|
||
One of the crucial abilities of `Core` today, spawning features, is being | ||
removed! This comes as a result of distilling the features that the `tokio` | ||
crate provides to the bare bones, which is just I/O object registration (e.g. | ||
interaction with `epoll` and friends). Spawning futures is quite common today | ||
though, so we of course still want to support it! | ||
|
||
This support will be added through the `futures` crate rather than the | ||
`tokio` crate itself. Namely the `futures` crate effectively already has an | ||
efficient implementation of spawning futures through the `FuturesUnordered` | ||
type. To expose this, the `futures` crate will grow the following type in the | ||
`futures::unsync` module: | ||
|
||
```rust | ||
// in futures::unsync | ||
|
||
pub struct CurrentThread; | ||
|
||
impl CurrentThread { | ||
/// Spawns a new future to get executed on the current thread. | ||
/// | ||
/// This future is added to a thread-local list of futures. This list of | ||
/// futures will be "completed in the background" when the current thread is | ||
/// otherwise blocked waiting for futures-related work. For example calls to | ||
/// `Future::wait` will by default attempt to run futures on this list. In | ||
/// addition, external runtimes like the `tokio` crate will also execute | ||
/// this list of futures in the `Core::run` method. | ||
/// | ||
/// Note that this can be a dangerous method to call if you don't know what | ||
/// thread you're being invoked from. The thread local list of futures is | ||
/// not guaranteed to be moving forward, which could cause the spawned | ||
/// future here to become inert. It's recommended think carefully when | ||
/// calling this method and either ensure that you're running on a thread | ||
/// that's moving the list forward or otherwise document that your API | ||
/// requires itself to be in such a context. | ||
/// | ||
/// Also note that the `future` provided here will be entirely executed on | ||
/// the current thread. This means that execution of any one future will | ||
/// block execution of any other future on this list. You'll want to | ||
/// accordingly ensure that none of the work for the future here involves | ||
/// blocking the thread for too long! | ||
pub fn spawn<F>(&self, future: F) | ||
where F: Future<Item = (), Error = ()> + 'static; | ||
|
||
/// Attempts to complete the thread-local list of futures. | ||
/// | ||
/// This API is provided for *runtimes* to try to move the thread-local list | ||
/// of futures forward. Each call to this function will perform as much work | ||
/// as possible as it can on the thread-local list of futures. | ||
/// | ||
/// The `notify` argument here and `id` are passed with similar semantics to | ||
/// the `Spawn::poll_future_notify` method. | ||
/// | ||
/// In general unless you're implementing a runtime for futures you won't | ||
/// have to worry about calling this method. | ||
pub fn poll<T>(&self, notify: &T, id: usize) | ||
where T: Clone + Into<NotifyHandle>; | ||
} | ||
|
||
impl<F> Executor<F> for CurrentThread | ||
where F: Future<Item = (), Error = ()> + 'static | ||
{ | ||
// ... | ||
} | ||
``` | ||
|
||
The purpose of this type is to retain the ability to spawn futures referencing | ||
`Rc` and other non-`Send` data in an efficient fashion. The `FuturesUnordered` | ||
primitive used to implement this should exhibit similar performance | ||
characteristics as the current implementation in `tokio-core`. | ||
|
||
## Fate of other Tokio crates | ||
|
||
This RFC proposed deprecating the `tokio-core`, `tokio-proto`, and | ||
`tokio-service` crates. The `tokio-proto` and `tokio-service` crates will be | ||
deprecated without replacement for now, but it's expected that higher level | ||
application frameworks will quickly fill in the gap here. For example most users | ||
already aren't using `tokio-proto` (it's just an implementation detail of | ||
`hyper`). Crates like `hyper`'s exposure of `tokio-service` will be refactored | ||
in an application-specific manner, likely involving a Hyper-specific trait. | ||
|
||
The `tokio-core` crate, however, will be deprecated with the `tokio` crate as a | ||
replacement. The migration path should be relatively straightforward, | ||
essentially deleting all `Core`, `Remote`, and `Handle` usage while taking | ||
advantage of `CurrentThread`. The `tokio-core` crate will be *reimplemented*, | ||
however, in terms of the new `tokio` crate. A new API will be added to extract a | ||
`tokio::reactor::Core` from a `tokio_core::reactor::Handle`. That is, you can | ||
acquire non-deprecated structures from deprecated structures. | ||
|
||
## API migration guidelines | ||
|
||
Crates with `tokio-core` as a public dependency will be able to upgrade to | ||
`tokio` without an API breaking change. The migration path looks like: | ||
|
||
* If `TcpStream` is exposed in a public API, the API should be generalizable to | ||
an instance of `AsyncRead + AsyncWrite`. The traits here should in theory be | ||
enough to not require a `TcpStream` specifically. | ||
* Similarly if a `UdpSocket` is exposed it's intended that `Sink` and `Stream` | ||
can likely be used instead. | ||
* If `Handle` is exposed then backwards compatibility can be retained by | ||
converting the argument to a generic value. A trait, `AsHandle`, will be added | ||
to the `tokio` crate for ease of performing this transition. Both the old | ||
`tokio-core` `Handle` and the new `Handle` will implement this trait. | ||
|
||
Our hope is that crates which have a major version bump themselves on the | ||
horizon can take advantage of the opportunity to remove the trait bound and | ||
simply take a `tokio` `Handle` argument. | ||
|
||
# Drawbacks | ||
[drawbacks]: #drawbacks | ||
|
||
This change is inevitably going to create a fairly large amount of churn. The | ||
migration from `tokio-core` to `tokio` will take time, and furthermore the | ||
deprecation of `tokio-proto` and `tokio-service` will take some time to | ||
propagate throughout the ecosystem. | ||
|
||
Despite this, however, we view the churn as worth the benefits we'll reap on the | ||
other end. This change will empower us to greatly focus the documentation of | ||
Tokio solely on the I/O related aspects and free up future library and framework | ||
design space. This should also bring much more clarity to "what is the Tokio | ||
project?" and at what layer you enter at, as there's only one layer! | ||
|
||
# Rationale and Alternatives | ||
[alternatives]: #alternatives | ||
|
||
As with any API design there's a lot of various alternatives to consider, but | ||
for now this'll be limited to more focused alternatives to the current "general | ||
design" rather than more radical alternatives that may look entirely different. | ||
|
||
TODO: more docs here | ||
|
||
# Unresolved questions | ||
[unresolved]: #unresolved-questions | ||
|
||
N/A |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
What's the interaction here if a user called
copy(reader, writer).wait()
in here? Is it like today in that it's a big no-no, and it will likely deadlock the event loop?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 think it'd do the same before/after the rfc, right? In that it'd deadlock the event loop no matter what?
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.
Ok, just wanted to be sure, since it sounded like
wait
would maybe do some extra "magic", and I wondered if that changed how it would work in here.