Leaf futures
Runtimes create leaf futures, which represent a resource such as a socket.
This is an example of a leaf future:
let mut stream = tokio::net::TcpStream::connect(“127.0.0.1:3000”);
Operations on these resources, such as a reading from a socket, will be non-blocking and return a future, which we call a leaf future since it’s the future that 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 are the kind of futures we as users of a runtime write ourselves using the async keyword to create a task that 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 represent a set of operations. Often, such a task will await a leaf future as one of many operations to complete the task.
This is an example of a non-leaf future:
let non_leaf = async {
let mut stream =
TcpStream::connect(“127.0.0.1:3000”).await.unwrap();
println!(“connected!”);
let result = stream.write(b”hello world\n”).await;
println!(“message sent!”);
…
};
The two highlighted lines indicate points where we pause the execution, yield control to a runtime, and eventually resume. In contrast to leaf futures, these kinds of futures do not themselves represent an I/O resource. When we poll them, they will run until they get to a leaf future that returns Pending and then yields control to the scheduler (which is a part of what we call the runtime).
Runtimes
Languages such as C#, JavaScript, Java, Go, and many others come with a runtime for handling concurrency. So, if you’re used to one of those languages, this will seem a bit strange to you. 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 that provides this for you.
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 you’ll see that there are several similarities between this kind of runtime, 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.