audit pass on waker + generators

This commit is contained in:
cfsamson
2020-04-06 15:20:02 +02:00
parent 9c2079c839
commit 16cd145661
9 changed files with 155 additions and 146 deletions

View File

@@ -2,10 +2,11 @@
> **Overview:**
>
> - High level introduction to concurrency in Rust
> - Knowing what Rust provides and not when working with async code
> - Understanding why we need a runtime-library in Rust
> - Getting pointers to further reading on concurrency in general
> - Get a high level introduction to concurrency in Rust
> - Know what Rust provides and not when working with async code
> - Get to know why we need a runtime-library in Rust
> - Understand the difference between "leaf-future" and a "non-leaf-future"
> - Get insight on how to handle CPU intensive tasks
## Futures
@@ -90,7 +91,7 @@ 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.
Quite a bit of complexity attributed to `Futures` are actually complexity rooted
Quite a bit of complexity attributed to `Futures` is 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
@@ -136,14 +137,14 @@ take a look at this async block using pseudo-rust as example:
```rust, ignore
let non_leaf = async {
let mut stream = TcpStream::connect("127.0.0.1:3000").await.unwrap(); <-- yield
let mut stream = TcpStream::connect("127.0.0.1:3000").await.unwrap(); // <-- yield
// request a large dataset
let result = stream.write(get_dataset_request).await.unwrap(); <-- yield
let result = stream.write(get_dataset_request).await.unwrap(); // <-- yield
// wait for the dataset
let mut response = vec![];
stream.read(&mut response).await.unwrap(); <-- yield
stream.read(&mut response).await.unwrap(); // <-- yield
// do some CPU-intensive analysis on the dataset
let report = analyzer::analyze_data(response).unwrap();
@@ -167,12 +168,13 @@ resolves when the task is finished. We could `await` this leaf-future like any
other future.
2. The runtime could have some kind of supervisor that monitors how much time
different tasks take, and move the executor itself to a different thread.
different tasks take, and move the executor itself to a different thread so it can
continue to run even though our `analyzer` task is blocking the original executor thread.
3. You can create a reactor yourself which is compatible with the runtime which
does the analysis any way you see fit, and returns a Future which can be awaited.
Now #1 is the usual way of handling this, but some executors implement #2 as well.
Now, #1 is the usual way of handling this, but some executors implement #2 as well.
The problem with #2 is that if you switch runtime you need to make sure that it
supports this kind of supervision as well or else you will end up blocking the
executor.
@@ -187,8 +189,9 @@ can either perform CPU-intensive tasks or "blocking" tasks which is not supporte
by the runtime.
Now, armed with this knowledge you are already on a good way for understanding
Futures, but we're not gonna stop yet, there is lots of details to cover. Take a
break or a cup of coffe and get ready as we go for a deep dive in the next chapters.
Futures, but we're not gonna stop yet, there is lots of details to cover.
Take a break or a cup of coffe and get ready as we go for a deep dive in the next chapters.
## Bonus section

View File

@@ -2,9 +2,9 @@
> **Overview:**
>
> - 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
> - Understand how the Waker object is constructed
> - Learn how the runtime know when a leaf-future can resume
> - Learn the basics of dynamic dispatch and trait objects
>
> The `Waker` type is described as part of [RFC#2592][rfc2592].
@@ -27,7 +27,7 @@ extend the ecosystem with new leaf-level tasks.
## 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
flexibility for future evolutions of the API in Rust. The context can for example hold
task-local storage and provide space for debugging hooks in later iterations.
## Understanding the `Waker`
@@ -97,10 +97,6 @@ we use _dynamic dispatch_.
Let's explain this in code instead of words by implementing our own trait
object from these parts:
>This is an example of _editable_ code. You can change everything in the example
and try to run it. If you want to go back, press the undo symbol. Keep an eye
out for these as we go forward. Many examples will be editable.
```rust
// A reference to a trait object is a fat pointer: (data_ptr, vtable_ptr)
trait Test {
@@ -162,12 +158,10 @@ fn main() {
}
```
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.
like we do here. The way we create it is slightly different, but now that you know
how regular trait objects work you will probably recognize what we're doing which
makes it much less mysterious.
## Bonus section

View File

@@ -2,9 +2,9 @@
>**Overview:**
>
>- Understandi how the async/await syntax works since it's how `await` is implemented
>- Know why we need `Pin`
>- Understand why Rusts async model is very efficient
>- Understand how the async/await syntax works under the hood
>- See first hand why we need `Pin`
>- Understand what makes Rusts async model very memory efficient
>
>The motivation for `Generators` can be found in [RFC#2033][rfc2033]. It's very
>well written and I can recommend reading through it (it talks as much about
@@ -45,11 +45,11 @@ let future = Connection::connect(conn_str).and_then(|conn| {
}).collect::<Vec<SomeStruct>>()
});
let rows: Result<Vec<SomeStruct>, SomeLibraryError> = block_on(future).unwrap();
let rows: Result<Vec<SomeStruct>, SomeLibraryError> = block_on(future);
```
While an effective solution there are mainly three downsides I'll focus on:
**There are mainly three downsides I'll focus on using this technique:**
1. The error messages produced could be extremely long and arcane
2. Not optimal memory usage
@@ -88,10 +88,12 @@ async fn myfn() {
}
```
Async in Rust is implemented using Generators. So to understand how Async really
Async in Rust is implemented using Generators. So to understand how async really
works we need to understand generators first. Generators in Rust are implemented
as state machines. The memory footprint of a chain of computations is only
defined by the largest footprint of what the largest step require.
as state machines.
The memory footprint of a chain of computations is defined by _the largest footprint
that a single step requires_.
That means that adding steps to a chain of computations might not require any
increased memory at all and it's one of the reasons why Futures and Async in
@@ -230,7 +232,7 @@ unfamiliar. We could add some syntactic sugar like implementing the `Iterator`
trait for our generators which would let us do this:
```rust, ignore
for val in generator {
while let Some(val) = generator.next() {
println!("{}", val);
}
```
@@ -307,7 +309,7 @@ into itself.
As you'll notice, this compiles just fine!
```rust, ignore
```rust
enum GeneratorState<Y, R> {
Yielded(Y),
Complete(R),
@@ -323,7 +325,7 @@ enum GeneratorA {
Enter,
Yield1 {
to_borrow: String,
borrowed: *const String,
borrowed: *const String, // NB! This is now a raw pointer!
},
Exit,
}
@@ -344,7 +346,7 @@ impl Generator for GeneratorA {
let res = borrowed.len();
*self = GeneratorA::Yield1 {to_borrow, borrowed: std::ptr::null()};
// We set the self-reference here
// NB! And we set the pointer to reference the to_borrow string here
if let GeneratorA::Yield1 {to_borrow, borrowed} = self {
*borrowed = to_borrow;
}
@@ -375,8 +377,8 @@ let mut gen = move || {
};
```
Below is an example of how we could run this state-machine. But there is still
one huge problem with this:
Below is an example of how we could run this state-machine and as you see it
does what we'd expect. But there is still one huge problem with this:
```rust
pub fn main() {
@@ -451,7 +453,7 @@ pub fn main() {
# }
```
The problem however is that in safe Rust we can still do this:
The problem is that in safe Rust we can still do this:
_Run the code and compare the results. Do you see the problem?_
```rust, should_panic
@@ -532,7 +534,7 @@ pub fn main() {
# }
```
Wait? What happened to "Hello"?
Wait? What happened to "Hello"? And why did our code segfault?
Turns out that while the example above compiles just fine, we expose consumers
of this this API to both possible undefined behavior and other memory errors
@@ -540,9 +542,17 @@ while using just safe Rust. This is a big problem!
> I've actually forced the code above to use the nightly version of the compiler.
> If you run [the example above on the playground](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=5cbe9897c0e23a502afd2740c7e78b98),
> you'll see that it runs without panic on the current stable (1.42.0) but
> you'll see that it runs without panicing on the current stable (1.42.0) but
> panics on the current nightly (1.44.0). Scary!
We'll explain exactly what happened here using a slightly simpler example in the next
chapter and we'll fix our generator using `Pin` so don't worry, you'll see exactly
what goes wrong and see how `Pin` can help us deal with self-referential types safely in a
second.
Before we go and explain the problem in detail, let's finish off this chapter
by looking at how generators and the async keyword is related.
## Async blocks and generators
Futures in Rust are implemented as state machines much the same way Generators
@@ -563,7 +573,7 @@ let mut gen = move || {
Compare that with a similar example using async blocks:
```rust, ignore
let mut fut = async || {
let mut fut = async {
let to_borrow = String::from("Hello");
let borrowed = &to_borrow;
SomeResource::some_task().await;
@@ -577,11 +587,7 @@ have. The states of a Rust Futures is either: `Pending` or `Ready`.
An async block will return a `Future` instead of a `Generator`, however, the way
a Future works and the way a Generator work internally is similar.
The same goes for the challenges of borrowin across yield/await points.
We'll explain exactly what happened using a slightly simpler example in the next
chapter and we'll fix our generator using `Pin` so join me as we explore
the last topic before we implement our main Futures example.
The same goes for the challenges of borrowing across yield/await points.
## Bonus section - self referential generators in Rust today