From 70c4020059b9df05d9fea2469a445f92450d89ee Mon Sep 17 00:00:00 2001 From: Carl Fredrik Samson Date: Mon, 24 Feb 2020 23:25:48 +0100 Subject: [PATCH] continued version 2 --- book/1_background_information.html | 159 ++++++++---- ...rait_objects.html => 2_waker_context.html} | 80 ++++-- book/3_generators_pin.html | 21 +- book/4_pin.html | 17 +- book/6_future_example.html | 17 +- book/8_finished_example.html | 17 +- book/conclusion.html | 17 +- book/highlight.css | 120 +++++---- book/index.html | 36 ++- book/introduction.html | 36 ++- book/print.html | 235 ++++++++++++------ book/searchindex.js | 2 +- book/searchindex.json | 2 +- src/1_background_information.md | 91 +++---- src/2_waker_context.md | 68 ++++- theme/highlight.css | 120 +++++---- 16 files changed, 716 insertions(+), 322 deletions(-) rename book/{2_trait_objects.html => 2_waker_context.html} (77%) diff --git a/book/1_background_information.html b/book/1_background_information.html index 7dce638..e07de5b 100644 --- a/book/1_background_information.html +++ b/book/1_background_information.html @@ -78,7 +78,7 @@ @@ -163,60 +163,102 @@ information that will help demystify some of the concepts we encounter.

Actually, after going through these concepts, implementing futures will seem pretty simple. I promise.

-

Async in Rust

-

Let's get some of the common roadblocks out of the way first.

-

Async in Rust is different from most other languages in the sense that Rust -has a very lightweight runtime.

-

Languages like C#, JavaScript, Java and GO, already includes a runtime -for handling concurrency. So if you come from one of those languages this will +

Futures

+

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. +
  3. 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.
  4. +
  5. 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.
  6. +
+

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.

+

Leaf futures

+

Runtimes create leaf futures which represents a resource like a socket.

+
// stream is a **leaf-future**
+let mut stream = tokio::net::TcpStream::connect("127.0.0.1:3000");
+
+

Operations on these resources, like a Read on a socket, will be non-blocking +and return a future which we call a leaf future since it's the future which +we're actually waiting on.

+

It's unlikely that you'll implement a leaf future yourself unless you're writing +a runtime, but we'll go through how they're constructed in this book as well.

+

It's also unlikely that you'll pass a leaf-future to a runtime and run it to +completion alone as you'll understand by reading the next paragraph.

+

Non-leaf-futures

+

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.

+

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.

+
// 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!");
+    ...
+};
+
+

The key to these tasks is that they're able to yield control to the runtime's +scheduler and then resume execution again where it left off at a later point.

+

In contrast to leaf futures, these kind of futures does not themselves represent +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

+

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 seem a bit strange to you.

-

In Rust you will have to make an active choice about which runtime to use.

+

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 +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. +
  3. The Reactor
  4. +
+

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:

+

What Rust's standard library takes care of

    -
  1. The definition of an interruptible task
  2. -
  3. An efficient technique to start, suspend, resume and store tasks which are -executed concurrently.
  4. -
  5. A defined way to wake up a suspended task
  6. +
  7. A common interface representing an operation which will be completed in the +future through the Future trait.
  8. +
  9. An ergonomic way of creating tasks which can be suspended and resumed through +the async and await keywords.
  10. +
  11. 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. Well, in Rust we normally divide the runtime into two parts:

- -

Reactors create leaf Futures, and provides things like non-blocking sockets, -an event queue and so on.

-

Executors, accepts one or more asynchronous tasks called Futures and takes -care of actually running the code we write, suspend the tasks when they're -waiting for I/O and resume them.

-

In theory, we could choose one Reactor and one Executor that have nothing -to do with each other besides that one creates leaf Futures and the other one -runs them, but in reality today you'll most often get both in a Runtime.

-

There are mainly two such runtimes today async_std and tokio.

-

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.

-

Futures 1.0 and Futures 3.0

-

I'll not spend too much time on this, but it feels wrong to not mention that -there have been several iterations on how async should work in Rust.

-

Futures 3.0 works with the relatively new async/await syntax in Rust and -it's what we'll learn.

-

Now, since this is rather recent, you can encounter creates that use Futures 1.0 -still. This will get resolved in time, but unfortunately it's not always easy -to know in advance.

-

A good sign is that if you're required to use combinators like and_then then -you're using Futures 1.0.

-

While they're not directly compatible, there is a tool that let's you relatively -easily convert a Future 1.0 to a Future 3.0 and vice a versa. You can find -all you need in the futures-rs crate and all -information you need here.

-

First things first

+

Bonus section

If you find the concepts of concurrency and async programming confusing in general, I know where you're coming from and I have written some resources to try to give a high level overview that will make it easier to learn Rusts @@ -243,7 +285,7 @@ it needs to be, so go on and read these chapters if you feel a bit unsure.

- @@ -261,7 +303,7 @@ it needs to be, so go on and read these chapters if you feel a bit unsure.

- @@ -270,6 +312,21 @@ it needs to be, so go on and read these chapters if you feel a bit unsure.

+ + + diff --git a/book/2_trait_objects.html b/book/2_waker_context.html similarity index 77% rename from book/2_trait_objects.html rename to book/2_waker_context.html index c721c02..3aed484 100644 --- a/book/2_trait_objects.html +++ b/book/2_waker_context.html @@ -3,7 +3,7 @@ - Trait objects and fat pointers - Futures Explained in 200 Lines of Rust + Waker and Context - Futures Explained in 200 Lines of Rust @@ -78,7 +78,7 @@ @@ -149,27 +149,48 @@
-

Trait objects and fat pointers

+

Waker and Context

Relevant for:

  • Understanding how the Waker object is constructed
  • -
  • Getting a basic feel for "type erased" objects and what they are
  • -
  • Learning the basics of dynamic dispatch
  • +
  • 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.

-

Trait objects and dynamic dispatch

-

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 +

The Waker

+

The Waker type allows for a loose coupling between the reactor-part and the executor-part of a runtime.

+

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.

+
+

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.

+

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.

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):

# use std::mem::size_of;
 trait SomeTrait { }
 
@@ -205,7 +226,8 @@ information.

  • 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:

    @@ -273,9 +295,20 @@ 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.

    @@ -315,6 +348,21 @@ it is will make this much less mysterious.

    + + + diff --git a/book/3_generators_pin.html b/book/3_generators_pin.html index d18ab59..78c3299 100644 --- a/book/3_generators_pin.html +++ b/book/3_generators_pin.html @@ -78,7 +78,7 @@ @@ -666,7 +666,7 @@ pub fn main() {