Some programming languages provide an abstraction over concurrent code execution that doesn’t require operating system threads. It requires an integrated framework that provides a task scheduler and asynchronous calls to the operating system. It’s based on coroutines and futures.

The main advantage of coroutine based programming is that you don’t have to manage operating system threads. Coroutines executed in cooperative tasks don’t even require a multithreaded environment. But it can benefit from operating system thread by using a thread pool for task execution.

Rust’s coroutines are suitable for both high-performance and embedded use cases. They are compiled into fixed-size objects without dynamically growing stacks. But let’s not worry about it for now.

Asynchronous blocks

A naive rewrite of threaded code into a task running and async block of code.

use std::{time::Duration, time::SystemTime};
use tokio::task;
use tokio::sync::mpsc::{Sender, channel};

fn delayed_value(sleep: u64, value: i32, sender: Sender<String>) -> task::JoinHandle<()> {
    task::spawn(async move {
        tokio::time::sleep(Duration::from_secs(sleep)).await;
        sender.send(format!("Answer is {}.", value)).await.unwrap();
    })
}

#[tokio::main]
async fn main() {
    let (sender, mut receiver) = channel(32);

    let task1 = delayed_value(3, 42, sender.clone());
    let task2 = delayed_value(2, 43, sender.clone());

    println!("{:?}", SystemTime::now());
    println!("{}", receiver.recv().await.unwrap());
    println!("{:?}", SystemTime::now());
    println!("{}", receiver.recv().await.unwrap());
    println!("{:?}", SystemTime::now());

    task1.await.unwrap();
    task2.await.unwrap();
}

Asynchronous functions

Much simpler programs can be made of asynchronous functions.

use std::{time::Duration, time::SystemTime};
use tokio::join;

async fn delayed_value(sleep: u64, value: i32) -> String {
    tokio::time::sleep(Duration::from_secs(sleep)).await;
    format!("Answer is {}.", value)
}

#[tokio::main]
async fn main() {
    println!("{:?}", SystemTime::now());
    let (result1, result2) = join!(
        delayed_value(3, 42),
        delayed_value(2, 43),
    );
    println!("{} {}", result1, result2);
    println!("{:?}", SystemTime::now());
}

The main reasons to use asynchronous functions are flexibility and readability. You write the code once, you decide the threading configuration later.

Scheduler and communication

Look at the documentation for the Tokio library. It is the most popular scheduler and I/O system for running asynchronous code in Rust. It provides many tools similar to the std library but integrated into the Tokio scheduler.

The folloing example shows how to download data over HTTP using just the TCP connectivity features from Tokio. There are higher level libraries based on Tokio.

use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream;

async fn download(host: &str) -> Result<String, std::io::Error> {
    let target = (host, 80);
    let mut stream = TcpStream::connect(target).await?;

    stream.write_all(b"GET / HTTP/1.0\r\n\r\n").await?;

    let mut content = Vec::new();
    stream.read_to_end(&mut content).await?;

    Ok(String::from_utf8(content).expect("UTF-8 conversion failed."))
}

#[tokio::main]
async fn main() {
    let download1 = tokio::spawn(download("example.com"));
    let download2 = tokio::spawn(download("example.net"));

    let result1 = download1
        .await
        .expect("First download crashed.")
        .expect("First download failed.");
    let result2 = download2
        .await
        .expect("Second download crashed.")
        .expect("Second download failed.");

    println!("{:?}, {:?}", result1, result2);
}

Don’t forget to use the right dependencies in Rust.

[dependencies]
tokio = { version = "1", features = ["full"] }