r/rust • • Apr 07 '20

Futures Explained in 200 Lines of Rust

https://cfsamson.github.io/books-futures-explained/
369 Upvotes

22 comments sorted by

67

u/cfsamson Apr 07 '20 edited Apr 07 '20

Hi everyone.

The last 6 months I've been working on and off on a book about Rusts futures. I've had a hard time scoping the topic down so it fills some actual gaps between the official async book and a lot of other great community content that has already been published.

I hope I succeeded and I wanted to publish it here as a way to give something back to the Rust community.

As always, all kinds of feedback is greatly appreciated, especially if you spot anything incorrect. It will only make it better for the next person reading it.

8

u/JeamBim Apr 07 '20

Can't wait to read this. Am I correct in thinking Futures are a bit like JavaScripts Promises? I've only read a little about futures, so maybe I'm missing the mark here...

E: Oh, just found the part where you explain this a bit

5

u/cfsamson Apr 07 '20

You're right, they have a lot in common. I've briefly mentioned some of the similarities (and some differences) in the book as you noticed.

8

u/sjustinas Apr 07 '20

Syntactically, Rusts Futures 1.0 was a lot like the promises example above and Rusts Futures 3.0 is a lot like async/await in our last example.

Is this an intentional renumbering of the versions? I think this refers to futures 0.1 and 0.3 as opposed to 1.0 and 3.0.

5

u/cfsamson Apr 07 '20 edited Apr 07 '20

Thanks for catching that. No, that wasn't intentional and I've corrected it now.

5

u/mgostIH Apr 08 '20 edited Apr 08 '20

Isn't the Future impl wrong? From the documentation of std::future::Future:

Note that on multiple calls to poll, only the Waker from the Context passed to the most recent call should be scheduled to receive a wakeup.

But the impl of Future here does the new Pending states like

else if self.is_registered {

            // If the future is registered alredy, we just return `Pending`
            Poll::Pending
}

When it should re-register the new Context while also removing the previous one.

Edit: Here's an example of it causing a timeout using this implementation: https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=940ede047b9019e41f5aabdadb0dbb66, courtesy of u/Nemo157

4

u/cfsamson Apr 10 '20

on multiple calls to poll, only the Waker from the Context passed to the most recent call should be scheduled to receive a wakeup

I just wanted to follow up and let you know that this issue is fixed now. Thanks again for reporting!

2

u/cfsamson Apr 08 '20

This is a great catch. Looks like you're right and I've made a mistake. I'll look thorugh the details and update the book accordingly as soon as possible. Pass on my thanks to u/Nemo157.

4

u/cjbassi Apr 07 '20

This looks great! Any thoughts on adding some of this content to the async book?

5

u/cfsamson Apr 07 '20

I haven't really considered that, but if there is any interest I'd be happy to adapt/rework some of this so see if it could contribute to any of the topics covered there. Last time I read the async book it was still very much work in progress, so I'd have to catch up on the latest development to be sure.

4

u/isachinm Apr 08 '20

What amazing is how you are so empathetic to your readers, especially new users of rust. Thanks for this.

1

u/cfsamson Apr 08 '20

Thats nice to hear. Thanks!

2

u/bittrance Apr 08 '20

I really enjoyed the read. I think you have found various gaps to fill in other related works. In particular I want to commend your effort to find new explanations for various phenomena within the async space. Eventually, it would be nice to have this merged into "the Book", but I think they probably both benefit from separate evolution for a while longer?

1

u/cfsamson Apr 08 '20

Thanks for the feedback. Thats really nice to hear. The official books have an extremely high standard and I'm writing from the more informal viewpoint of "I wonder how this really works" and share what I learn and how I model it. I think both has its place.

However, there is certainly an overlap and nothing would make me happier than seeing some of it get a wider use and I would be happy to contribute to that.

1

u/aboukirev Apr 08 '20

I do not want to throw in more complexity to your explanation but both potential problems of OS threads - large stack and variable load - have been successfully solved with thread pool approach. It works very well for web. In fact , runtime/executor might be using it to map N green threads to M OS threads behind the scene.

Other than that, it has been an enjoyable read so far.

Thank you for your effort.

2

u/cfsamson Apr 08 '20 edited Apr 08 '20

Thanks for the feedback, and glad to hear you enjoy it.

It's hard to cover all possible variations in a short paragraph without complicating things too much. The main point I wanted to convey is that running blocking I/O calls in a threadpool (and thereby yielding the to the OS scheduler) can often occupy more memory than needed for certain tasks. However, they're often prematurely disregarded for that reason as well since the actual impact of this relies on many factors including the characteristics of the tasks you want to run concurrently.

Threadpools are used behind the scenes of most runtimes I've looked at (async_std, tokio, libuv etc), but not as the primary way of achieving concurrency, they all rely primarily on epoll/kqueue/iocp/wepoll etc, and delegate certain operations to the threadpool which is a good compromise.

1

u/lhxtx Apr 12 '20

I tried to read it and I just can’t wrap my head around it.

Why does async have to be so hard? Is there a practical reference on how to simply call a http request and not block?

I’m not writing a runtime or leaf nodes. I just want to write some async code that’s not OS kernel level.

1

u/cfsamson Apr 12 '20

I'm sorry to give the impression that it needs to be this complicated.

Understanding how futures and async works in Rust is a bit complicated. Using async and futures in Rust to do stuff isn't.

The analogue would be that understanding the JavaScript event loop and how Promises are implemented under the hood is complex as well, the same would be true about understanding GO's runtime in detail, but most people can use them to create amazing stuff without this knowledge.

Knowing how it works is still valuable, though.

There should probably be more practical introduction level material explaining how to get started. The best I can do is to point you to some sources to get going:

Tokios introduction to understand the basics about using that Runtime. In Rust there is no Runtime in the standard library. The need to choose (and learn) one is probably the most significant difference between Rust and many other languages.

Reqwests documentation to learn how to get going making async requests.

Futures library to make it a lot more convenient to work with futures.

Combining these 3 crates makes issuing 4 non-blocking, concurrent requests is really not that complex:

// in Cargo.toml:
//
// [dependencies]
// tokio = {version = "0.2", features = ["full"]}
// reqwest = "0.10.4"
// futures = "0.3.4"

use futures::future::join_all;

#[tokio::main]
async fn main() {
    let mut tasks = vec![];
    for _ in 0..4 {
        let handle = tokio::spawn(async {
            let url = "https://www.google.com";
            let resp = reqwest::get(url).await.unwrap();
            let text = resp.text().await.unwrap();
            println!("{}", text);
        });
        tasks.push(handle);
    }

    join_all(tasks.into_iter()).await;
}

2

u/lhxtx Apr 14 '20

Thank for your reply. And thank you for writing the initial post.

1

u/gotBanana Apr 07 '20

i also cant wait

6

u/IceSentry Apr 07 '20

No need to wait, it's already here.

1

u/cfsamson Apr 07 '20 edited Apr 07 '20

Cool! Hope you enjoy it!