Async Rust

A gentle introduction
A group of black crabs walking towards the words "async/.await" with a larger crab poised over it. The Rust logo is in the top left corner.

Hot off the heels of RustFest Barcelona and the stabilization of async/.await, I think it's safe to say that one of Rust's most anticipated language features has finally landed. And for that occasion (and because I've had some trouble understanding certain bits of it myself), I wanted to write a little introduction to asynchronous programming in Rust. We'll be creating a super simple application that fetches some data from the internet using the our newfound async abilities. The stabilization of async/.await also coincides nicely with another event I'm excited about: This week saw the release of the most recent entries in the mainline Pokémon games, Pokémon Sword and Shield, and because I'm a bit too busy to pick them up just yet, I'll make do by fetching data from the PokéAPI for now.

I'm assuming some base knowledge of Rust's syntax and ecosystem, but I hope that this is pretty accessible even to people very new to the community.

But before diving into the coding part, let's cover some basic concepts of asynchronous programming and how it might be a bit different in Rust than what you'd expect.

What does async mean?

In Rust, when we talk about async, we're talking about running code concurrently, or having multiple overlapping (in time) computations run on a single thread. Multithreading is a related, but distinct concept. Multithreading is ideal for when you've got computationally intensive tasks (so-called CPU-bound tasks) that can be spread across multiple, separated cores. Concurrent programming is better suited for when the task spends a lot of time waiting, such as for a response from a server. These tasks are called IO-bound.

So asynchronous programming lets us run multiple of these IO-bound computations at the same time on a single thread. They can run at the same time because when they're waiting for a response, they're just idle, so we can let the computer keep working on something that isn't waiting. When we reach a point where we need the result of an asynchronous computation, we must .await it. In Rust, values that are 'awaitable' are known as 'futures'.

Rusty weirdness

async in Rust may be a bit different from what you're used to in other languages. Having done asynchronous coding mostly in JavaScript and C#, it certainly was to me. Here's a few key things to understand:

An async function does not (necessarily) start executing immediately

To start an asynchronous function, you must either .await it or launch a task using an executor (we'll get to that in a moment). Until this happens, all you have is a Future that has not started. Let's look at an example to make it clearer:

     use async_std::task;
     // ^ we need this for task spawning

     async fn negate_async(n: i32) -> i32 {
         println!("Negating {}", n);
         task::sleep(std::time::Duration::from_secs(5)).await;
         println!("Finished sleeping for {}!", n);
         n * -1
     }

     async fn f() -> i32 {
         let neg = negate_async(1);
         // ... nothing happens yet
         let neg_task = task::spawn(negate_async(2));
         // ^ this task /is/ started
         task::sleep(std::time::Duration::from_secs(1)).await;
         // we sleep for effect.

         neg.await + neg_task.await
         // ^ this starts the first task `neg`
         // and waits for both tasks to finish
     }

So in the above little code snippet, here's what's going on.

  • The first line imports async_std::task. There's more on this below, but we need an external library to run futures as the standard library does not come with an executor.
  • The async function negate_async takes as input a signed integer, sleeps for 5 seconds, and returns the negated version of that integer.
  • The async function f is more interesting:
    • The first line (let neg ...) creates a Future of the negate_async function and assigns it to the neg variable. Importantly, it does /not/ start executing yet.
    • The next line of code (let neg_task ...) uses the task::spawn function to start executing the Future returned by negate_async. Like with neg, the Future returned by negate_async is assigned to the neg_task variable.
    • Next: we sleep for a second. This is so that it will be obvious from the output when a task starts running.
    • Finally, we await both futures, add them together, and return them. By awaiting neg, we start executing the Future and run it to completion. Since neg_task has already been started, we just wait for it to finish.

So what's the result of this, then?

      Negating 2
      # <- there's a 1 second pause here
      Negating 1
      Finished sleeping for 2!
      Finished sleeping for 1!

As we can see, the second future, neg_task, started executing as soon as it was called---thanks to task::spawn---while neg did not start executing until it was awaited.

You need an external library to use async/.await

As was briefly alluded to above, you need to reach for an external library to do asynchronous programming in Rust. This took me a while to understand, as I'm used to it being part of the language experience. In Rust, however, you need a dedicated /executor/1. The executor is what takes care of executing the futures, polling them and returning the results when they're done. The standard library does not come with an executor, so we need to reach out to an external crate for this. There are a few ones to choose from, but the two most prominent ones are ~async-std~ (which we're using here) and ~tokio~.

A minimal async example!

Alright, let's get practical. This is the reason that I'm writing this post. As mentioned at the start, we'll be creating a super simple application that fetches some Pokémon data and prints it to the console. For preparation, make sure you've got at least version 1.39 of Rust and cargo available.

Step 1: creating the application

Let's create a new application! Simply run this command in your preferred directory:

      cargo new async-basics

Step 2: Dependencies

We're going to be using ~async-std~ for spawning tasks, and ~surf~ to fetch data from the API. Let's add them to the Cargo.toml file. Your whole file should look something like this:

    [package]
    name = "async-basics"
    version = "0.1.0"
    authors = ["Your Name <your.email@provider.tld>"]
    edition = "2018"

    [dependencies]
    async-std = "1"
    surf = "1"

Nice! This is going swimmingly!

Step 3: Fetch data

Okay, final step. Let's modify the main.rs file. We'll make it as simple as possible. Here's what we want to use:

     use async_std::task;
     use surf;

     // fetch data from a url and return the results as a string.
     // if an error occurs, return the error.
     async fn fetch(url: &str) -> Result<String, surf::Exception> {
         surf::get(url).recv_string().await
     }

     // execute the fetch function and print the results
     async fn execute() {
         match fetch("https://pokeapi.co/api/v2/move/surf").await {
             Ok(s) => println!("Fetched results: {:#?}", s),
             Err(e) => println!("Got an error: {:?}", e),
         };
     }

     fn main() {
         task::block_on(execute());
         // ^ start the future and wait for it to finish
     }

That's all the code you need. In fact, it's more than what you need, as some parts have been broken up for legibility. Let's walk through it!

~use~ statements
Nothing exciting here. Just importing the crates we declared in the Cargo.toml file: surf and async_std.
~fetch~
This is simply a thin wrapper around the surf::get function which returns either the payload as a String or an Exception if something went wrong.
~execute~
This function calls fetch with the endpoint for the move Surf, waits for the result to return, and then matches on the result. If everything went well: print the output. Else: print the error.
~main~
main simply kicks off execute and waits for it to finish. task::block_on is a synchronous counterpart to task::spawn that starts an asynchronous operation, but blocks until it has finished. Because the main function can't itself be async (at least not at the time of writing), we can't use .await in it, but we can block on asynchronous operations.

Step 4: Extend it!

Hey, you made it this far; congrats! That's all I really have in store for you for this one, but if you want to play around a bit more, how about adding ~serde~ and try using surf's recv_json<T> instead? If you'd rather keep looking at async/.await, how about performing multiple requests simultaneously? Or how about making a PokéAPI CLI? (Ooh, that sounds like fun! Hit me up if you're doing this; I want in!)

Parting words and resources

So there you have it, dear reader. I hope you have found this useful. async/.await is finally stabilized and it feels like we've taken a major leap forward. I'm very much looking forward to seeing what happens in the coming months and what the community makes of this.

If you're looking for more resources on async Rust, be sure to check out the Async Book. I also recommend the async-std book for some extra insights.

Until next time: take care!

Footnotes


A 'prompt' saying 'dirname' superimposed onto a background of blurry text. The command line control icon is barely visible in the lower right corner.

When you need to get the directory name of a file but you only have the full path to the file, what do you do? You could use some fancy parameter expansion, sure, but you might run into some weird edge cases. Personally, I'd suggest just using dirname.

If you're familiar with Python and the os library, the behavior is the same as os.path.dirname, but even if you're not, it's still pretty straightforward. Let's look at the man page for the command. It states that given a path, it will output it "with its last non-slash component and trailing slashes removed." If the path contains no path separators, it will output ., which is the current directory.

So in a nutshell, that means it will normalize path/to/file.extension to path/to, and f.txt to .. Because it removes trailing slashes and because it's not dependent on any file extensions, you can use it repeatedly to move further and further up the tree:

  some/long/path/to/file.extension
  -> some/long/path/to
  -> some/long/path
  -> some/long
  -> some
  -> .

So when is this useful? Well, if you've got something in your path and you want to find out what else is in that directory, you could do something like this:

ls $(dirname $(which ))

Maybe you want to navigate to that directory or open it in some other command? Yeah, you can do that too.

If you're still not convinced that you should use dirname, check out this amazing stack overflow reply about some of the differences between parameter expansion and dirname.


Rust 2020

Compile-time constraints, ergonomics, and docs
The Rust logo with "20" on either side. An ominous shadow in the shape of ferris spans across the background.

Something, something, hindsight.

The end of the year is fast approaching and that, my friends, means that it's time for a deluge of end-of-year Rust blogs. The Rust core team has put out a call for blogs to help decide where to take the language next, as they do every year. I've been quite invested in Rust for a few years now, so I thought it was about time I joined the fray.

Considering that I spend a disproportionately large amount of time thinking and reading about Rust compared to how much I actually use it, do take my thoughts and wishes here with a grain of salt; they may not be what the community needs the most, it's just what I'd want in my ideal, little world.

Stabilize slice patterns (#![feature(slice_patterns)])

This is the one language feature I find myself wanting stabilized more than any other. Coming from languages like Haskell---and to a lesser extent Python and JavaScript---that have simple ways to destructure a list of values, doing this can feel quite clunky in Rust. Especially if all you want to do is get the head and the tail of a Vec or a slice. Yes, the ~split first~ function on slice does do that, but using it is tricky. In particular, how you handle destructured lists using case of statements in Haskell feels really ergonomic and intuitive to me.

Luckily, it seems we're headed in that direction. There is a tracking issue for the slice_patterns feature on GitHub, and save for some remaining compiler issues, it should be about ready to go in. For now, you can check it out on the nightly channel with the feature toggle #![feature(slice_patterns)].

What's more, because rust matches on slices and not linked lists like in Haskell, we get some extra goodies, such as being able to extract not only the first $n$ items, but also the last $m$ items. Here's a little playground link that I put together demonstrating some cool uses for slice patterns. (Note: you probably can do this in Haskell somehow---a little bit of searching tells me the ViewPatterns compiler extension might do the trick---but it's not baked into the base language like this.)

Easy async (documentation!)

I was holding a Rust workshop for some colleagues recently and one of them asked me to create an example of how you'd fetch web data using Rust. I thought this would be a great chance to have a little sneak peek at async/await before it stabilizes and get familiar with it: I figured I'd try and write a super simple application that fetches some data from an endpoint and dumps it to the terminal. However, I could not make it compile, and if it compiled, it didn't run properly (presumably because the future was never started).

What I'd like to see is more information on how to do basic async programming in Rust. The Async Book has a chapter on it, but I still find it confusing with all the different traits and unstable features. I've tried following the example with the Hyper server to make a request, but I keep running into trait bound errors. I'm sure if I sat down and spent some more time on it, it would work, but it feels like it's missing that 'get started in 2 minutes' section.

I'm sure this will improve very quickly over the next few weeks with the stabilization of async/await, but for now it's a hurdle.

Generic Associoted Types (GATs) and const generics

I've lumped these two together because I've not really followed either particularly closely, but reading up on them for the purpose of this post, I'm suddenly quite excited by both. In general I'm all for anything that makes the type system more expressive and allows us to create more compile-time constraints.

GATs
Const generics

In summary

I already had some ideas in mind, so I thought writing this post would be quick and easy, but I ended up diving down multiple rabbit holes, and in the end it took much longer than expected. That said, I learned a lot along the way, and I'm even more excited about the language and where we're headed now.

Apart from this short wish list, I also think it's important that we stay aware of the community and keep them in mind. As has been mentioned in a number of other blogs, let's try and make Rust sustainable for everyone: maintainers, core team, working groups, and community members alike. Let's also try and move forward with game development, GUI development, and web development. We're already great. Now let's be even better. And no matter where we go from here, I'm confident that as long as the community stays warm, welcoming, and inclusive, we'll have something to be proud of.

Much love ❤️