continued version 2
This commit is contained in:
@@ -20,6 +20,20 @@ So what is a future?
|
||||
A future is a representation of some operation which will complete in the
|
||||
future.
|
||||
|
||||
Async in Rust uses a `Poll` based approach, in which an asynchronous task will
|
||||
have three phases.
|
||||
|
||||
1. **The Poll phase.** A Future is polled which result in the task progressing until
|
||||
a point where it can no longer make progress. We often refer to the part of the
|
||||
runtime which polls a Future as an executor.
|
||||
2. **The Wait phase.** An event source, most often referred to as a reactor,
|
||||
registers that a Future is waiting for an event to happen and makes sure that it
|
||||
will wake the Future when that event is ready.
|
||||
3. **The Wake phase.** The event happens and the Future is woken up. It's now up
|
||||
to the executor which polled the Future in step 1 to schedule the future to be
|
||||
polled again and make further progress until it completes or reaches a new point
|
||||
where it can't make further progress and the cycle repeats.
|
||||
|
||||
Now, when we talk about futures I find it useful to make a distinction between
|
||||
**non-leaf** futures and **leaf** futures early on because in practice they're
|
||||
pretty different from one another.
|
||||
@@ -49,18 +63,18 @@ Non-leaf-futures is the kind of futures we as _users_ of a runtime writes
|
||||
ourselves using the `async` keyword to create a **task** which can be run on the
|
||||
executor.
|
||||
|
||||
This is an important distinction since these futures represents a
|
||||
_set of operations_. Often, such a task will `await` a leaf future as one of
|
||||
many operations to complete the task.
|
||||
The bulk of an async program will consist of non-leaf-futures, which are a kind
|
||||
of pause-able computation. This is an important distinction since these futures represents a _set of operations_. Often, such a task will `await` a leaf future
|
||||
as one of many operations to complete the task.
|
||||
|
||||
```rust
|
||||
```rust, ignore, noplaypen
|
||||
// Non-leaf-future
|
||||
let non_leaf = async {
|
||||
let mut stream = TcpStream::connect("127.0.0.1:3000").await.unwrap();// <- yield
|
||||
println!("connected!");
|
||||
let result = stream.write(b"hello world\n").await; // <- yield
|
||||
println!("message sent!");
|
||||
// ...
|
||||
...
|
||||
};
|
||||
```
|
||||
|
||||
@@ -72,22 +86,7 @@ an I/O resource. When we poll these futures we either run some code or we yield
|
||||
to the scheduler while waiting for some resource to signal us that it's ready so
|
||||
we can resume where we left off.
|
||||
|
||||
### Runtimes
|
||||
|
||||
Quite a bit of complexity attributed to `Futures` are actually complexity rooted
|
||||
in runtimes. Creating an efficient runtime is hard.
|
||||
|
||||
Learning how to use one correctly can require quite a bit of effort as well, but
|
||||
you'll see that there are several similarities between these kind of runtimes so
|
||||
learning one makes learning the next much easier.
|
||||
|
||||
The difference between Rust and other languages is that you have to make an
|
||||
active choice when it comes to picking a runtime. Most often you'll just use
|
||||
the one provided for you.
|
||||
|
||||
## Async in Rust
|
||||
|
||||
Let's get some of the common roadblocks out of the way first.
|
||||
## Runtimes
|
||||
|
||||
Languages like C#, JavaScript, Java, GO and many others comes with a runtime
|
||||
for handling concurrency. So if you come from one of those languages this will
|
||||
@@ -97,9 +96,33 @@ Rust is different from these languages in the sense that Rust doesn't come with
|
||||
a runtime for handling concurrency, so you need to use a library which provide
|
||||
this for you.
|
||||
|
||||
In other words you'll have to make an active choice about which runtime to use
|
||||
which will of course seem foreign if the environment you come from provides one
|
||||
which "everybody" uses.
|
||||
Quite a bit of complexity attributed to `Futures` are actually complexity rooted
|
||||
in runtimes. Creating an efficient runtime is hard.
|
||||
|
||||
Learning how to use one correctly requires quite a bit of effort as well, but
|
||||
you'll see that there are several similarities between these kind of runtimes so
|
||||
learning one makes learning the next much easier.
|
||||
|
||||
The difference between Rust and other languages is that you have to make an
|
||||
active choice when it comes to picking a runtime. Most often, in other languages
|
||||
you'll just use the one provided for you.
|
||||
|
||||
An async runtime can be divided into two parts:
|
||||
|
||||
1. The Executor
|
||||
2. The Reactor
|
||||
|
||||
When Rusts Futures were designed there was a desire to separate the job of
|
||||
notifying a `Future` that it can do more work, and actually doing the work
|
||||
on the `Future`.
|
||||
|
||||
You can think of the former as the reactor's job, and the latter as the
|
||||
executors job. These two parts of a runtime interacts using the `Waker` type.
|
||||
|
||||
The two most popular runtimes for `Futures` as of writing this is:
|
||||
|
||||
- [async-std](https://github.com/async-rs/async-std)
|
||||
- [Tokio](https://github.com/tokio-rs/tokio)
|
||||
|
||||
### What Rust's standard library takes care of
|
||||
|
||||
@@ -107,29 +130,11 @@ which "everybody" uses.
|
||||
future through the `Future` trait.
|
||||
2. An ergonomic way of creating tasks which can be suspended and resumed through
|
||||
the `async` and `await` keywords.
|
||||
3. A defined interface wake up a suspended task through the `Waker` trait.
|
||||
3. A defined interface wake up a suspended task through the `Waker` type.
|
||||
|
||||
That's really what Rusts standard library does. As you see there is no definition
|
||||
of non-blocking I/O, how these tasks are created or how they're run.
|
||||
|
||||
### What you need to find elsewhere
|
||||
|
||||
A runtime, often just referred to as an `Executor`.
|
||||
|
||||
There are mainly two such runtimes in wide use in the community today
|
||||
[async_std][async_std] and [tokio][tokio].
|
||||
|
||||
Executors, accepts one or more asynchronous tasks (`Futures`) and takes
|
||||
care of actually running the code we write, suspend the tasks when they're
|
||||
waiting for I/O and resume them when they can make progress.
|
||||
|
||||
>Now, you might stumble upon articles/comments which mentions both an `Executor`
|
||||
and an `Reactor` (also referred to as a `Driver`) as if they're well defined
|
||||
concepts you need to know about. This is not true. In practice today you'll only
|
||||
interface with the runtime, which provides leaf futures which actually wait for
|
||||
some I/O operation, and the executor where
|
||||
|
||||
|
||||
## Bonus section
|
||||
|
||||
If you find the concepts of concurrency and async programming confusing in
|
||||
|
||||
@@ -5,24 +5,50 @@
|
||||
> - Understanding how the Waker object is constructed
|
||||
> - Learning how the runtime know when a leaf-future can resume
|
||||
> - Learning the basics of dynamic dispatch and trait objects
|
||||
>
|
||||
> The `Waker` type is described as part of [RFC#2592][rfc2592].
|
||||
|
||||
## The Waker
|
||||
|
||||
The `Waker` trait is an interface where a
|
||||
The `Waker` type allows for a loose coupling between the reactor-part and the executor-part of a runtime.
|
||||
|
||||
One of the most confusing things we encounter when implementing our own `Futures`
|
||||
is how we implement a `Waker` . Creating a `Waker` involves creating a `vtable`
|
||||
which allows us to use dynamic dispatch to call methods on a _type erased_ trait
|
||||
By having a wake up mechanism that is _not_ tied to the thing that executes
|
||||
the future, runtime-implementors can come up with interesting new wake-up
|
||||
mechanisms. An example of this can be spawning a thread to do some work that
|
||||
eventually notifies the future, completely independent of the current runtime.
|
||||
|
||||
Without a waker, the executor would be the _only_ way to notify a running
|
||||
task, whereas with the waker, we get a loose coupling where it's easy to
|
||||
extend the ecosystem with new leaf-level tasks.
|
||||
|
||||
> If you want to read more about the reasoning behind the `Waker` type I can
|
||||
> recommend [Withoutboats articles series about them](https://boats.gitlab.io/blog/post/wakers-i/).
|
||||
|
||||
## The Context type
|
||||
|
||||
As the docs state as of now this type only wrapps a `Waker`, but it gives some
|
||||
flexibility for future evolutions of the API in Rust. The context can hold
|
||||
task-local storage and provide space for debugging hooks in later iterations.
|
||||
|
||||
## Understanding the `Waker`
|
||||
|
||||
One of the most confusing things we encounter when implementing our own `Futures`
|
||||
is how we implement a `Waker` . Creating a `Waker` involves creating a `vtable`
|
||||
which allows us to use dynamic dispatch to call methods on a _type erased_ trait
|
||||
object we construct our selves.
|
||||
|
||||
>If you want to know more about dynamic dispatch in Rust I can recommend an article written by Adam Schwalm called [Exploring Dynamic Dispatch in Rust](https://alschwalm.com/blog/static/2017/03/07/exploring-dynamic-dispatch-in-rust/).
|
||||
>If you want to know more about dynamic dispatch in Rust I can recommend an
|
||||
article written by Adam Schwalm called [Exploring Dynamic Dispatch in Rust](https://alschwalm.com/blog/static/2017/03/07/exploring-dynamic-dispatch-in-rust/).
|
||||
|
||||
Let's explain this a bit more in detail.
|
||||
|
||||
## Fat pointers in Rust
|
||||
|
||||
Let's take a look at the size of some different pointer types in Rust. If we
|
||||
run the following code. _(You'll have to press "play" to see the output)_:
|
||||
To get a better understanding of how we implement the `Waker` in Rust, we need
|
||||
to take a step back and talk about some fundamentals. Let's start by taking a
|
||||
look at the size of some different pointer types in Rust.
|
||||
|
||||
Run the following code _(You'll have to press "play" to see the output)_:
|
||||
|
||||
``` rust
|
||||
# use std::mem::size_of;
|
||||
@@ -65,7 +91,8 @@ The layout for a pointer to a _trait object_ looks like this:
|
||||
- The second 8 bytes points to the `vtable` for the trait object
|
||||
|
||||
The reason for this is to allow us to refer to an object we know nothing about
|
||||
except that it implements the methods defined by our trait. To accomplish this we use _dynamic dispatch_.
|
||||
except that it implements the methods defined by our trait. To accomplish this
|
||||
we use _dynamic dispatch_.
|
||||
|
||||
Let's explain this in code instead of words by implementing our own trait
|
||||
object from these parts:
|
||||
@@ -136,6 +163,25 @@ fn main() {
|
||||
|
||||
```
|
||||
|
||||
The reason we go through this will be clear later on when we implement our own
|
||||
`Waker` we'll actually set up a `vtable` like we do here to and knowing what
|
||||
it is will make this much less mysterious.
|
||||
Now that you know this you also know why how we implement the `Waker` type
|
||||
in Rust.
|
||||
|
||||
Later on, when we implement our own `Waker` we'll actually set up a `vtable`
|
||||
like we do here to and knowing why we do that and how it works will make this
|
||||
much less mysterious.
|
||||
|
||||
## Bonus section
|
||||
|
||||
You might wonder why the `Waker` was implemented like this and not just as a
|
||||
normal trait?
|
||||
|
||||
The reason is flexibility. Implementing the Waker the way we do here gives a lot
|
||||
of flexibility of choosing what memory management scheme to use.
|
||||
|
||||
The "normal" way is by using an `Arc` to use reference count keep track of when
|
||||
a Waker object can be dropped. However, this is not the only way, you could also
|
||||
use purely global functions and state, or any other way you wish.
|
||||
|
||||
This leaves a lot of options on the table for runtime implementors.
|
||||
|
||||
[rfc2592]:https://github.com/rust-lang/rfcs/blob/master/text/2592-futures.md#waking-up
|
||||
Reference in New Issue
Block a user