Le but de ce TP est d'apprendre à utiliser
Les threads peuvent être implémentés dans l'espace utilisateur, sans le support du noyau. Les choses suivantes se produisent si nous implémentons un thread dans l'espace utilisateur.
La plupart des programmes informatiques sont exécutés dans le même ordre dans lequel ils sont écrits. La première ligne s'exécute, puis la suivante, et ainsi de suite. Avec la programmation synchrone, lorsqu'un programme rencontre une opération qui ne peut pas être terminée immédiatement, il se bloque jusqu'à ce que l'opération se termine. Par exemple, l'établissement d'une connexion TCP nécessite un échange avec un pair sur le réseau, ce qui peut prendre un temps considérable. Pendant ce temps, le fil est bloqué.
Avec la programmation asynchrone, les opérations qui ne peuvent pas se terminer immédiatement sont suspendues en arrière-plan. Le thread n'est pas bloqué et peut continuer à exécuter d'autres choses. Une fois l'opération terminée, la tâche n'est plus suspendue et continue le traitement là où il s'était arrêté. Notre exemple précédent n'a qu'une seule tâche, donc rien ne se passe pendant qu'il est suspendu, mais les programmes asynchrones ont généralement de nombreuses tâches de ce type.
Bien que la programmation asynchrone puisse aboutir à des applications plus rapides, elle aboutit souvent à des programmes beaucoup plus compliqués. Le programmeur doit suivre tous les états nécessaires pour reprendre le travail une fois l'opération asynchrone terminée. Historiquement, il s'agit d'une tâche fastidieuse et source d'erreurs.
Tokio est un runtime asynchrone pour le langage de programmation Rust. Il fournit les blocs de construction nécessaires à l'écriture d'applications réseau. Il offre la flexibilité de cibler une large gamme de systèmes, des grands serveurs avec des dizaines de cœurs aux petits appareils embarqués.
À un niveau élevé, Tokio fournit quelques composants majeurs :
Lorsque vous écrivez votre application de manière asynchrone, vous lui permettez de bien mieux évoluer en réduisant le coût de faire plusieurs choses en même temps. Cependant, le code Rust asynchrone ne s'exécute pas seul, vous devez donc choisir un runtime pour l'exécuter. La bibliothèque Tokio est l'environnement d'exécution le plus largement utilisé, dépassant tous les autres environnements d'exécution combinés.
De plus, Tokio fournit de nombreux utilitaires utiles. Lors de l'écriture de code asynchrone, vous ne pouvez pas utiliser les API de blocage ordinaires fournies par la bibliothèque standard Rust et devez plutôt en utiliser des versions asynchrones. Ces versions alternatives sont fournies par Tokio, reflétant l'API de la bibliothèque standard Rust là où cela a du sens.
Redis est souvent appelé serveur de structures de données. Cela signifie que Redis donne accès à des structures de données mutables via un ensemble de commandes, qui sont envoyées à l'aide d'un modèle serveur-client avec des sockets TCP et un protocole simple. Ainsi, différents processus peuvent interroger et modifier les mêmes structures de données de manière partagée.
Pour ce tutoriel, nous utilisons une version Redis simplifiée pour Rust, mini-redis-server.
Rust implémente la programmation asynchrone à l'aide d'une fonctionnalité appelée async/wait. Les fonctions qui effectuent des opérations asynchrones sont étiquetées avec le mot-clé async.
La définition async fn ressemble à une fonction synchrone normale, mais fonctionne de manière asynchrone. Rust transforme le async fn au moment de la compilation en une routine qui fonctionne de manière asynchrone. Tous les appels à .wait
dans le async fn cèdent le contrôle au thread. Le thread peut effectuer d'autres tâches pendant que l'opération s'exécute en arrière-plan.
An async fn
is used as we want to enter an asynchronous context. However, asynchronous functions must be executed by a runtime. The runtime contains the asynchronous task scheduler, provides evented I/O, timers, etc. The runtime does not automatically start, so the main function needs to start it.
The #[tokio::main]
function is a macro. It transforms the async fn main()
into a synchronous fn main()
that initializes a runtime instance and executes the async main function.
Par exemple:
#[tokio::main] async fn main() { println!("hello"); }
est transformee en:
fn main() { let mut rt = tokio::runtime::Runtime::new().unwrap(); rt.block_on(async { println!("hello"); }) }
La concurrence et le parallélisme ne sont pas la même chose. Si vous alternez entre deux tâches, vous travaillez sur les deux tâches simultanément, mais pas en parallèle. Pour qu'il soit qualifié de parallèle, vous auriez besoin de deux personnes, une dédiée à chaque tâche.
Pour traiter les connexions simultanément, une nouvelle tâche est générée pour chaque connexion entrante. La connexion est traitée sur cette tâche.
La boucle d'acceptation devient :
use tokio::net::TcpListener; #[tokio::main] async fn main() { let listener = TcpListener::bind("127.0.0.1:6379").await.unwrap(); loop { let (socket, _) = listener.accept().await.unwrap(); // A new task is spawned for each inbound socket. The socket is // moved to the new task and processed there. tokio::spawn(async move { process(socket).await; }); } }
Une task Tokio est un thread vert asynchrone. Ils sont créés en passant un bloc asynchrone à tokio::spawn
. La fonction tokio::spawn
renvoie un JoinHandle
, que l'appelant peut utiliser pour interagir avec la tâche générée. Le bloc async peut avoir une valeur de retour. L'appelant peut obtenir la valeur de retour en utilisant .wait
sur le JoinHandle
.
#[tokio::main] async fn main() { let handle = tokio::spawn(async { // Do some async work "return value" }); // Do some other work let out = handle.await.unwrap(); println!("GOT {}", out); }
En attente sur JoinHandle renvoie un résultat. Lorsqu'une tâche rencontre une erreur lors de son exécution, le JoinHandle renvoie un Err. Cela se produit lorsque la tâche panique ou si la tâche est annulée de force par l'arrêt de l'environnement d'exécution.
Les tâches sont l'unité d'exécution gérée par le planificateur. Générer la tâche la soumet au planificateur de Tokio, qui s'assure ensuite que la tâche s'exécute lorsqu'elle a du travail à faire. La tâche générée peut être exécutée sur le même thread que celui où elle a été générée, ou elle peut s'exécuter sur un thread d'exécution différent. La tâche peut également être déplacée entre les threads après avoir été générée.
Les tâches dans Tokio sont très légères. Sous le capot, ils ne nécessitent qu'une seule allocation et 64 octets de mémoire. Les applications doivent se sentir libres de générer des milliers, voire des millions de tâches.
cargo install mini-redis
avant de resoudre les exercices.
mini-redis-server
.