« Rust : Tokio » : différence entre les versions

De Justine's wiki
Aller à la navigation Aller à la recherche
Aucun résumé des modifications
Aucun résumé des modifications
Ligne 136 : Ligne 136 :


On va avoir besoin de sockets TCP entrants sur le port 6379. On le fait avec tokio::net::TcpListener.  
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.
<quote>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.</quote>

Version du 19 août 2024 à 14:34

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. <quote>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.</quote>