How to Use Tokio with Rust

Practical guide to asynchronous operations

Altimetrik Poland Tech Blog
4 min readFeb 17, 2023
Source: https://pixabay.com/photos/highway-speed-car-motion-lights-821487/

Introduction

We live in a world where online systems handle thousands of requests per second. Doing this efficiently is a key element for many companies to succeed in their business, but such solutions need the right tools.

In this article we will look at the Tokio library. It is a software library/runtime environment for the Rust programming language. It supports asynchronous I/O, networking, scheduling, timers and much more. The main concept of this library is concurrency and executing multiple tasks at the same time.

Syntax async/await and runtime

Many modern languages use the async/await syntax. This syntax allows you to write asynchronous, non-blocking functions that look like synchronous functions. Such functions are easier to read and analyze than other alternatives. The async/await syntax is supported in Rust as of version 1.39.0 (November 2019).

How we use this syntax?

async fn main() {  
another_function().await;
}

The keyword “async” is used in front of an asynchronous function.

The keyword “await” is used when you want to pause the execution of the source code. It can only be used inside the async function.

How does it work?

When we use async and await the compiler generates a state machine in the background.

The execution of await does not block the thread on which it is executed.

1. When the await result is available, execution continues.

2. When the await result is not available, control returns to the caller of the async function. This allows other parts of the program to run while the async function waits. Later, thread execution will resume from the await function.

Why do we need runtime?

When we call async functions from the top level of our program, we need a mechanism to query the function (state machine handle). This mechanism is called runtime or executor. The standard Rust library does not have such a mechanism. There are several runtimes that are developed by the community.

The most widely used is Tokio https://crates.io/crates/tokio.

Other runtimes to use are async-std (https://crates.io/crates/async-std) and smol (https://crates.io/crates/smol).

Source: https://pixabay.com/photos/code-coding-computer-data-1839406/

Example

Let’s create an example to demonstrate how the Tokio library works. We will create a simple TCP server that can handle multiple requests at the same time.

First, we need to create a new project. The following command should be executed from the command line. It creates the tokio_example folder and the structure of the simple project.

cargo new tokio_example 

Then we need to go to a new folder and add the Tokio library to our project. The following command will use the latest version of Tokio.

cd tokio_example 
cargo add tokio

We need to make sure that the Cargo.toml file has the following content. This will allow us to use all the features of the Tokio library.

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

Use the below source code in the main.rs file.

use tokio::io::{AsyncReadExt, AsyncWriteExt}; 
use tokio::net::{TcpListener, TcpStream};
use tokio::time::{sleep, Duration};

#[tokio::main]
async fn main() {
let listener = TcpListener::bind("127.0.0.1:5432".to_string()).await.unwrap();
loop {
let (socket, _) = listener.accept().await.unwrap();
tokio::spawn(async move {
connection_handler(socket).await;
});
}
}
async fn connection_handler(mut socket: TcpStream) {
println!("processing started");
let mut buf = vec![0; 1024];
socket.read(&mut buf).await.unwrap();
sleep(Duration::from_millis(3000)).await;
let contents = "finished processing";
let response = format!(
"HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: {}\r\n\r\n{}",
contents.len(),
contents
);
socket.write(response.as_bytes()).await.unwrap();
socket.flush().await.unwrap();
println!("processing finished");
}

Now you can run the application.

cargo run

If you are using a web browser, you can open http://127.0.0.1:5432. The server receives the request and after 3 seconds sends a response to the browser. Such a delay is used to demonstrate that the main thread is not blocked and the requests are handled at the same time. You can open several tabs in the browser. A request from each of them would be handled concurrently.

Let’s look at the main elements from the main.rs file. I will focus only on the Tokio-specific blocks.

The below syntax is used to specify that our min() is asynchronous and will be executed by the Tokio runtime.

#[tokio::main] 
async fn main() {
}

The following code is used to spawn a new task. The keywords “async move” pass variables to the spawned context. It is necessary to pass the variable “socket”.

let (socket, _) = listener.accept().await.unwrap(); 
tokio::spawn(async move {
connection_handler(socket).await;
});

The next function calls await, so we need async in front of the function name.

async fn connection_handler(mut socket: TcpStream) { 
}

The following lines of code use await to avoid blocking the thread and return control to the caller.

let (socket, _) = listener.accept().await.unwrap(); 
sleep(Duration::from_millis(3000)).await;
socket.write(response.as_bytes()).await.unwrap();
socket.flush().await.unwrap();

Tokio ecosystem

Logo of the Tokio library. Source: https://en.wikipedia.org/wiki/Tokio_%28software%29#/media/File:Tokio_logo.svg

Tokio is not just runtime. It provides the building blocks for writing network applications. It gives you the flexibility to target a wide range of systems (from small embedded devices to large servers).

Some of the main components of Tokio:

  • A multithreaded runtime for executing asynchronous code
  • An asynchronous version of the standard library
  • A large ecosystem of libraries

Tokio libraries include:

  • Hyper — HTTP client and server libraries
  • Tonic — gRPC client and server libraries for exposing and consuming APIs over the network
  • Traceing — structured, event-based data collection and logging
  • Bytes — utilities for managing byte arrays

Resources:

Tokio main website https://tokio.rs

Tokio on Github https://github.com/tokio-rs/tokio

Totio create (binary library) https://crates.io/crates/tokio

Words by Artur Fraczak, Staff Engineer

Editing by Kinga Kuśnierz, Content Writer

--

--

Altimetrik Poland Tech Blog

This is a Technical Blog of Altimetrik Poland team. We focus on subjects like: Java, Data, Mobile, Blockchain and Recruitment. Waiting for your feedback!