Rust : Tokio
A propos de cette page
Il s'agit de mes notes issues du tuto d'utilisation de l'environnement Tokio : ici
Présentation
Tokio est un runtime asynchrone pour Rust, et fournit les outils pour créer des applications asynchrone utilisant le réseau. Les composants principaux sont:
- Un runtime multi-process pour l'exécution de code asynchrone
- Une version asynchrone de la librairie standard
- Un grand ecosystème de librairies associées.
Son rôle est avant tout d'accélerer les applications dans le cas où elles sont dépendante d'IO réseau en grande quantité, et pas dépendantes du CPU. Il n'est pas non plus intéressant pour accéder à un grand nombre de fichiers simultanément car les OS n'ont généralement pas d'API asynchrone pour les filesystems.
Setup
Le but du tutoriel est de montrer comment implémenter un client et un serveur Redis, avec un petit ensemble de commandes Redis. Ce projet s'appelle Mini-Redis et est sur Github.
Avec une version récente de Rust, on commence par le serveur mini-redis, qui nous permettra de tester notre client.
cargo install mini-redis //Lancer le serveur mini-redis-server //Depuis un autre terminal mini-redis-cli get foo //Doit renvoyer (nil)
Hello Tokio
On commence par créer une application très simple, qui va se connecter au serveur mini-redis et passer la clef "hello" à "world".
cargo new my-redis cd my-redis //Cargo.toml tokio = { version = "1", features = ["full"] } mini-redis = "0.4"
Puis dans le main.rs
use mini_redis::{client, Result}; #[tokio::main] async fn main() -> Result<()> { // Open a connection to the mini-redis address. let mut client = client::connect("127.0.0.1:6379").await?; // Set the key "hello" with value "world" client.set("hello", "world".into()).await?; // Get key "hello" let result = client.get("hello").await?; println!("got value from the server; result={:?}", result); Ok(()) }
En faisant un cargo run avec le serveur mini-redis fonctionnel dans un autre terminal, on a bien le résultat attendu.
En détail :
let mut client = client::connect("127.0.0.1:6379").await?;
Fonction fournie par mini-redis qui donne un handle sur un client tcp. L'opération est asynchrone, mais le code ressemble à du code synchrone; on sait qu'il est asynchrone grace à "await".
Programmation asynchrone ?
La plupart du temps, les programmes exécutés dans l'ordre dans lequel ils sont écrits. Si une tâche prend du temps, le thread est bloqué le temps que ça termine, ce qui peut être le cas pour une connexion TCP via laquelle un échange de données a lieu.
Avec la programmation asynchrone, les opération qui ne peuvent pas se terminer immédiatement vont en arrière plan. Le thread n'est pas bloqué et peut faire d'autres choses en attendant. Quand la tâche en arrière plan se termine, elle n'est plus suspendue et peut continuer. La programmation asynchrone peut permettre d'avoir des applications plus rapides, mais aussi bien plus compliquées. Elles forcent à gérer l'état des différentes tâches du programme.
Compile-time green threading (j'ai pas envie de traduire)
Rust implémente l'asynchrone avec les mots async et await. Les fonctions qui font de l'asynchrone sont marquées avec async:
pub async fn connect<T: ToSocketAddrs>(addr: T) -> Result<Client> //etc
Les fonctions en "async fn" sont traduites par Rust lors de la compilation vers des routines asynchrones. N'importe quel appel à .await dans la fonction asynchrone renvoie le contrôle des opérations au thread, afin qu'il puisse faire autre chose pendant que les opérations se terminent en fond.
Utiliser async / await
Les fonctions asynchrones sont appellées comme n'importe quelle autre fonction, mais ne renvoient pas une valeur représentant le résultat de leurs opérations. Elles renvoient une valeur qui représente l'opération. Il faut utiliser .await sur cette valeur afin d'obtenir le résultat. Exemple:
async fn say_world() { println!("world"); } #[tokio::main] async fn main() { // Calling `say_world()` does not execute the body of `say_world()`. let op = say_world(); // This println! comes first println!("hello"); // Calling `.await` on `op` starts executing `say_world`. op.await; }
Renvoie
hello world
La valeur de retour d'une "async fn" est un type anonyme qui implément le trait "Future".
Fonction main asynchrone
La fonction main utilisée ici est différente de ce qu'on trouve habituellement : elle est asynchrone et annotée avec "#[tokio::main]". Une fonction async est nécessaire car on veut entrer dans un environnement asynchrone. Cependant, elle doit être exécutée par un runtime, qui contient le task scheduler, les I/O, les timers, etc. C'est le rôle de la macro #[tokio::main].
Cette macro sert à transformer notre 'async fn main' en une 'fn main()' synchrone qui initialize une instance du runtime et lance la fonction main asynchrone.
Avec ça:
#[tokio::main] async fn main() { println!("hello"); }
On a en réalité
fn main() { let mut rt = tokio::runtime::Runtime::new().unwrap(); rt.block_on(async { println!("hello"); }) }
Features de Tokio
Tokio a beaucoup de fonctionnalités : TCP, UDP, sockets unix, etc. Nous avons ici utilisé la feature "full" pour l'exemple mais on peut en enlever pour alléger la compilation.
Spawning
On bouge le code précendent en example.
mkdir -p examples mv src/main.rs examples/hello-redis.rs
Sockets entrants
On va avoir besoin de sockets TCP entrants sur le port 6379. On le fait avec tokio::net::TcpListener.
Many of Tokio's types are named the same as their synchronous equivalent in the Rust standard library. When it makes sense, Tokio exposes the same APIs as std but using async fn.