Leçon 12 - Programmation asynchrone

P3 - Avancé

Découvrez async/await pour créer des applications réactives et performantes.

Introduction à la programmation asynchrone

La programmation asynchrone permet d'exécuter des opérations longues (requêtes réseau, accès disque, calculs complexes) sans bloquer le thread principal de votre application.

Pourquoi l'asynchrone ?

Syntaxe de base : async et await

C# utilise les mots-clés async et await pour simplifier la programmation asynchrone.

Méthode synchrone vs asynchrone

C#
using System; using System.Threading.Tasks; // ❌ Version SYNCHRONE (bloquante) static void TelechargerDonnees() { Console.WriteLine("Début du téléchargement..."); Thread.Sleep(3000); // Bloque pendant 3 secondes ! Console.WriteLine("Téléchargement terminé"); } // ✅ Version ASYNCHRONE (non bloquante) static async Task TelechargerDonneesAsync() { Console.WriteLine("Début du téléchargement..."); await Task.Delay(3000); // Ne bloque pas ! Console.WriteLine("Téléchargement terminé"); } // Utilisation static async Task Main(string[] args) { Console.WriteLine("Application démarrée"); // Version asynchrone - permet d'autres opérations pendant l'attente await TelechargerDonneesAsync(); Console.WriteLine("Application terminée"); }

Retourner des valeurs avec Task<T>

Pour retourner une valeur depuis une méthode asynchrone, utilisez Task<T> :

C#
using System; using System.Threading.Tasks; static async Task<int> CalculerAsync(int a, int b) { Console.WriteLine("Calcul en cours..."); await Task.Delay(1000); // Simulation d'un calcul long return a + b; } static async Task<string> ObtenirMessageAsync() { await Task.Delay(500); return "Bonjour depuis une méthode async!"; } // Utilisation static async Task Main(string[] args) { // Attendre le résultat int resultat = await CalculerAsync(5, 3); Console.WriteLine($"Résultat: {resultat}"); // 8 string message = await ObtenirMessageAsync(); Console.WriteLine(message); }

Exécuter plusieurs tâches en parallèle

Avec Task.WhenAll, vous pouvez exécuter plusieurs opérations asynchrones simultanément :

C#
static async Task<string> TelechargerFichier(string nom, int duree) { Console.WriteLine($"📥 Téléchargement de {nom} démarré..."); await Task.Delay(duree); Console.WriteLine($"✅ {nom} téléchargé"); return $"Contenu de {nom}"; } static async Task Main(string[] args) { var chrono = System.Diagnostics.Stopwatch.StartNew(); // ❌ Séquentiel (lent) - Total: 6 secondes // await TelechargerFichier("fichier1.txt", 2000); // await TelechargerFichier("fichier2.txt", 2000); // await TelechargerFichier("fichier3.txt", 2000); // ✅ Parallèle (rapide) - Total: 2 secondes Task<string> tache1 = TelechargerFichier("fichier1.txt", 2000); Task<string> tache2 = TelechargerFichier("fichier2.txt", 2000); Task<string> tache3 = TelechargerFichier("fichier3.txt", 2000); // Attendre que toutes les tâches soient terminées string[] resultats = await Task.WhenAll(tache1, tache2, tache3); chrono.Stop(); Console.WriteLine($"\n⏱️ Temps total: {chrono.ElapsedMilliseconds}ms"); foreach (var resultat in resultats) { Console.WriteLine($"- {resultat}"); } }

Task.WhenAny - Première tâche terminée

C#
static async Task<string> RequeteServeur(string serveur, int delai) { await Task.Delay(delai); return $"Réponse de {serveur}"; } static async Task Main(string[] args) { // Lancer 3 requêtes en parallèle Task<string> serveur1 = RequeteServeur("Serveur US", 1500); Task<string> serveur2 = RequeteServeur("Serveur EU", 800); Task<string> serveur3 = RequeteServeur("Serveur ASIA", 1200); // Attendre le premier qui répond Task<string> premierTermine = await Task.WhenAny(serveur1, serveur2, serveur3); string resultat = await premierTermine; Console.WriteLine($"✅ Premier résultat: {resultat}"); }

Gestion des erreurs avec async/await

C#
static async Task<int> DiviserAsync(int a, int b) { await Task.Delay(500); if (b == 0) throw new DivideByZeroException("Division par zéro!"); return a / b; } static async Task Main(string[] args) { try { int resultat1 = await DiviserAsync(10, 2); Console.WriteLine($"10 / 2 = {resultat1}"); // 5 int resultat2 = await DiviserAsync(10, 0); Console.WriteLine($"10 / 0 = {resultat2}"); // Ne s'exécute pas } catch (DivideByZeroException ex) { Console.WriteLine($"❌ Erreur: {ex.Message}"); } catch (Exception ex) { Console.WriteLine($"❌ Erreur inattendue: {ex.Message}"); } }

Annulation avec CancellationToken

Permet d'annuler des opérations longues en cours :

C#
using System.Threading; static async Task TacheLongue(CancellationToken token) { for (int i = 1; i <= 10; i++) { // Vérifier si l'annulation est demandée if (token.IsCancellationRequested) { Console.WriteLine("❌ Tâche annulée"); token.ThrowIfCancellationRequested(); } Console.WriteLine($"⏳ Progression: {i}/10"); await Task.Delay(1000, token); } Console.WriteLine("✅ Tâche terminée"); } static async Task Main(string[] args) { var cts = new CancellationTokenSource(); // Lancer la tâche Task tache = TacheLongue(cts.Token); // Annuler après 3 secondes await Task.Delay(3500); Console.WriteLine("🛑 Demande d'annulation..."); cts.Cancel(); try { await tache; } catch (OperationCanceledException) { Console.WriteLine("✅ Annulation confirmée"); } }

Exemple pratique : Téléchargement de fichiers

C#
using System.Net.Http; using System.IO; static readonly HttpClient client = new HttpClient(); static async Task<string> TelechargerPageAsync(string url) { try { Console.WriteLine($"📥 Téléchargement de {url}..."); string contenu = await client.GetStringAsync(url); Console.WriteLine($"✅ Téléchargement terminé ({contenu.Length} caractères)"); return contenu; } catch (HttpRequestException ex) { Console.WriteLine($"❌ Erreur: {ex.Message}"); return null; } } static async Task TelechargerPlusieursPages() { List<string> urls = new List<string> { "https://jsonplaceholder.typicode.com/posts/1", "https://jsonplaceholder.typicode.com/posts/2", "https://jsonplaceholder.typicode.com/posts/3" }; // Créer toutes les tâches var taches = urls.Select(url => TelechargerPageAsync(url)); // Exécuter en parallèle string[] resultats = await Task.WhenAll(taches); int succes = resultats.Count(r => r != null); Console.WriteLine($"\n✅ {succes}/{urls.Count} pages téléchargées avec succès"); }
Règles d'or avec async/await :
  • async tout le long : Si une méthode est async, toutes celles qui l'appellent devraient l'être aussi
  • Ne pas bloquer : N'utilisez jamais .Result ou .Wait(), utilisez toujours await
  • ConfigureAwait(false) : Dans les bibliothèques, utilisez-le pour améliorer les performances
  • Nommage : Suffixez vos méthodes async avec "Async"
  • Exception : Les exceptions sont capturées et retournées par la Task

Bonnes pratiques

C#
// ❌ MAUVAIS - Bloque le thread static void Mauvais() { var resultat = TelechargerPageAsync("https://example.com").Result; // Bloque! } // ✅ BON - Asynchrone de bout en bout static async Task Bon() { var resultat = await TelechargerPageAsync("https://example.com"); // Non bloquant } // ❌ MAUVAIS - async void (sauf pour les event handlers) static async void MauvaisAsync() { await Task.Delay(1000); } // ✅ BON - async Task static async Task BonAsync() { await Task.Delay(1000); } // ✅ Gestion du timeout static async Task<string> AvecTimeout(string url, int timeoutMs) { using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(timeoutMs)); try { return await client.GetStringAsync(url, cts.Token); } catch (OperationCanceledException) { Console.WriteLine("❌ Timeout dépassé"); return null; } }

Exercice pratique

Exercice :

Créez un système de traitement de commandes asynchrone :

  1. Une classe Commande avec numéro, montant, statut
  2. Simulez le traitement asynchrone de commandes (paiement, validation, expédition)
  3. Traitez plusieurs commandes en parallèle
  4. Affichez la progression en temps réel
  5. Gérez les erreurs (paiement refusé, stock insuffisant, etc.)
  6. Calculez le temps total de traitement

Points clés à retenir

Attention :

async/await ne crée pas automatiquement de nouveaux threads. Il optimise l'utilisation des threads existants. Pour du vrai parallélisme CPU-bound, utilisez Task.Run() ou Parallel.