Introduction à la programmation asynchrone en Rust
Les applications modernes exigent de plus en plus de hautes performances lors de l'interaction avec le réseau, le système de fichiers ou les bases de données. La programmation asynchrone en Rust permet de traiter efficacement des milliers de connexions simultanées sans créer un thread pour chaque tâche. Contrairement au multithreading, le code asynchrone utilise moins de mémoire et s'adapte mieux.
Rust propose une approche unique de l'asynchronisme, basée sur des abstractions à coût zéro. Cela signifie que vous ne payez que pour ce que vous utilisez — aucun surcoût pour les fonctionnalités inutilisées. Dans ce guide, nous allons décomposer les concepts clés et écrire des exemples pratiques qui vous aideront à commencer à écrire du code asynchrone en Rust dès aujourd'hui.
Fondamentaux d'async/await et Futures
Qu'est-ce qu'un Future ?
Future est un trait représentant une opération asynchrone qui peut se terminer dans le futur. En Rust, Future est une entité paresseuse : elle ne fait rien tant qu'elle n'est pas interrogée (poll). L'idée principale : au lieu de bloquer un thread, nous enregistrons une tâche et obtenons le résultat lorsqu'il est prêt.
use std::future::Future;use std::pin::Pin;use std::task::{Context, Poll};
struct MyFuture { ready: bool,}
impl Future for MyFuture { type Output = u32;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<u32> { if self.ready { Poll::Ready(42) } else { // Enregistrement du waker pour une nouvelle interrogation cx.waker().wake_by_ref(); Poll::Pending } }}async/await — sucre syntaxique
Le mot-clé async transforme une fonction ou un bloc en Future. await suspend l'exécution jusqu'à la fin du Future, en redonnant le contrôle au runtime.
use tokio::time::{sleep, Duration};
async fn fetch_data() -> String { // Simulation d'une requête réseau sleep(Duration::from_secs(2)).await; String::from("Données reçues")}
#[tokio::main]async fn main() { let result = fetch_data().await; println!("{}", result);}Cycles de vie et borrow checker
L'une des particularités de Rust est le contrôle strict des emprunts. Dans le code asynchrone, c'est particulièrement important car un Future peut vivre plus longtemps que les données empruntées. Utilisez async move pour capturer la propriété :
async fn process_data(data: Vec<u32>) -> u32 { data.iter().sum()}
#[tokio::main]async fn main() { let numbers = vec![1, 2, 3, 4, 5]; let sum = process_data(numbers).await; println!("Somme : {}", sum);}Travail pratique avec Tokio
Installation et configuration
Tokio est le runtime asynchrone le plus populaire pour Rust. Ajoutez dans Cargo.toml :
[dependencies]tokio = { version = "1", features = ["full"] }Création d'un serveur TCP asynchrone
Considérons un simple serveur echo qui accepte les connexions et renvoie les données reçues :
use tokio::net::TcpListener;use tokio::io::{AsyncReadExt, AsyncWriteExt};
#[tokio::main]async fn main() -> Result<(), Box<dyn std::error::Error>> { let listener = TcpListener::bind("127.0.0.1:8080").await?; println!("Serveur démarré sur le port 8080");
loop { let (mut socket, addr) = listener.accept().await?; println!("Nouvelle connexion de : {}", addr);
tokio::spawn(async move { let mut buf = [0; 1024]; loop { let n = socket.read(&mut buf).await.unwrap(); if n == 0 { break; } // Connexion fermée socket.write_all(&buf[..n]).await.unwra