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 ?
- Applications réactives : L'interface reste fluide pendant les opérations longues
- Performance : Libère les ressources pendant l'attente
- Scalabilité : Permet de gérer plus de requêtes simultanées
- Expérience utilisateur : Pas de blocage de l'interface
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
using System;
using System.Threading.Tasks;
static void TelechargerDonnees()
{
Console.WriteLine("Début du téléchargement...");
Thread.Sleep(3000);
Console.WriteLine("Téléchargement terminé");
}
static async Task TelechargerDonneesAsync()
{
Console.WriteLine("Début du téléchargement...");
await Task.Delay(3000);
Console.WriteLine("Téléchargement terminé");
}
static async Task Main(string[] args)
{
Console.WriteLine("Application démarrée");
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> :
using System;
using System.Threading.Tasks;
static async Task<int> CalculerAsync(int a, int b)
{
Console.WriteLine("Calcul en cours...");
await Task.Delay(1000);
return a + b;
}
static async Task<string> ObtenirMessageAsync()
{
await Task.Delay(500);
return "Bonjour depuis une méthode async!";
}
static async Task Main(string[] args)
{
int resultat = await CalculerAsync(5, 3);
Console.WriteLine($"Résultat: {resultat}");
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 :
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();
Task<string> tache1 = TelechargerFichier("fichier1.txt", 2000);
Task<string> tache2 = TelechargerFichier("fichier2.txt", 2000);
Task<string> tache3 = TelechargerFichier("fichier3.txt", 2000);
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
static async Task<string> RequeteServeur(string serveur, int delai)
{
await Task.Delay(delai);
return $"Réponse de {serveur}";
}
static async Task Main(string[] args)
{
Task<string> serveur1 = RequeteServeur("Serveur US", 1500);
Task<string> serveur2 = RequeteServeur("Serveur EU", 800);
Task<string> serveur3 = RequeteServeur("Serveur ASIA", 1200);
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
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}");
int resultat2 = await DiviserAsync(10, 0);
Console.WriteLine($"10 / 0 = {resultat2}");
}
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 :
using System.Threading;
static async Task TacheLongue(CancellationToken token)
{
for (int i = 1; i <= 10; i++)
{
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();
Task tache = TacheLongue(cts.Token);
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
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"
};
var taches = urls.Select(url => TelechargerPageAsync(url));
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
static void Mauvais()
{
var resultat = TelechargerPageAsync("https://example.com").Result;
}
static async Task Bon()
{
var resultat = await TelechargerPageAsync("https://example.com");
}
static async void MauvaisAsync()
{
await Task.Delay(1000);
}
static async Task BonAsync()
{
await Task.Delay(1000);
}
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 :
- Une classe Commande avec numéro, montant, statut
- Simulez le traitement asynchrone de commandes (paiement, validation, expédition)
- Traitez plusieurs commandes en parallèle
- Affichez la progression en temps réel
- Gérez les erreurs (paiement refusé, stock insuffisant, etc.)
- Calculez le temps total de traitement
Système de traitement de commandes asynchrone
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Linq;
namespace SystemeCommandes
{
public enum StatutCommande
{
EnAttente,
Paiement,
Validation,
Expedition,
Terminee,
Erreur
}
public class Commande
{
public int Numero { get; set; }
public decimal Montant { get; set; }
public StatutCommande Statut { get; set; }
public string MessageErreur { get; set; }
public DateTime DateCreation { get; set; }
public DateTime? DateTerminee { get; set; }
public override string ToString()
{
string icone = Statut switch
{
StatutCommande.EnAttente => "⏳",
StatutCommande.Paiement => "💳",
StatutCommande.Validation => "🔍",
StatutCommande.Expedition => "📦",
StatutCommande.Terminee => "✅",
StatutCommande.Erreur => "❌",
_ => ""
};
return $"{icone} Commande #{Numero} ({Montant:C}) - {Statut}";
}
}
public class ProcesseurCommandes
{
private static readonly Random random = new Random();
private static readonly object consoleLock = new object();
public static async Task<Commande> TraiterCommandeAsync(Commande commande)
{
try
{
AfficherProgression(commande);
commande.Statut = StatutCommande.Paiement;
AfficherProgression(commande);
await Task.Delay(1000);
if (random.Next(100) < 10)
{
throw new Exception("Paiement refusé");
}
commande.Statut = StatutCommande.Validation;
AfficherProgression(commande);
await Task.Delay(800);
if (random.Next(100) < 5)
{
throw new Exception("Stock insuffisant");
}
commande.Statut = StatutCommande.Expedition;
AfficherProgression(commande);
await Task.Delay(1200);
commande.Statut = StatutCommande.Terminee;
commande.DateTerminee = DateTime.Now;
AfficherProgression(commande);
return commande;
}
catch (Exception ex)
{
commande.Statut = StatutCommande.Erreur;
commande.MessageErreur = ex.Message;
AfficherProgression(commande);
return commande;
}
}
private static void AfficherProgression(Commande commande)
{
lock (consoleLock)
{
if (commande.Statut == StatutCommande.Erreur)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine($"{commande} - {commande.MessageErreur}");
}
else if (commande.Statut == StatutCommande.Terminee)
{
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine(commande);
}
else
{
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine(commande);
}
Console.ResetColor();
}
}
public static List<Commande> GenererCommandes(int nombre)
{
List<Commande> commandes = new List<Commande>();
for (int i = 1; i <= nombre; i++)
{
commandes.Add(new Commande
{
Numero = i,
Montant = (decimal)(random.NextDouble() * 500 + 10),
Statut = StatutCommande.EnAttente,
DateCreation = DateTime.Now
});
}
return commandes;
}
public static void AfficherStatistiques(List<Commande> commandes, TimeSpan duree)
{
int total = commandes.Count;
int reussies = commandes.Count(c => c.Statut == StatutCommande.Terminee);
int echouees = commandes.Count(c => c.Statut == StatutCommande.Erreur);
decimal montantTotal = commandes
.Where(c => c.Statut == StatutCommande.Terminee)
.Sum(c => c.Montant);
Console.WriteLine("\n╔══════════════════════════════════════╗");
Console.WriteLine("║ STATISTIQUES DE TRAITEMENT ║");
Console.WriteLine("╚══════════════════════════════════════╝");
Console.WriteLine($"Total de commandes: {total}");
Console.WriteLine($"✅ Réussies: {reussies} ({reussies * 100.0 / total:F1}%)");
Console.WriteLine($"❌ Échouées: {echouees} ({echouees * 100.0 / total:F1}%)");
Console.WriteLine($"💰 Montant total: {montantTotal:C}");
Console.WriteLine($"⏱️ Temps de traitement: {duree.TotalSeconds:F2}s");
Console.WriteLine($"⚡ Vitesse: {total / duree.TotalSeconds:F2} cmd/s");
if (echouees > 0)
{
Console.WriteLine("\n❌ Erreurs rencontrées:");
foreach (var commande in commandes.Where(c => c.Statut == StatutCommande.Erreur))
{
Console.WriteLine($" Commande #{commande.Numero}: {commande.MessageErreur}");
}
}
}
}
class Program
{
static async Task Main(string[] args)
{
Console.WriteLine("╔════════════════════════════════════════╗");
Console.WriteLine("║ SYSTÈME DE TRAITEMENT DE COMMANDES ║");
Console.WriteLine("╚════════════════════════════════════════╝\n");
int nombreCommandes = 10;
List<Commande> commandes = ProcesseurCommandes.GenererCommandes(nombreCommandes);
Console.WriteLine($"📋 {nombreCommandes} commandes générées\n");
var chrono = System.Diagnostics.Stopwatch.StartNew();
var taches = commandes.Select(c => ProcesseurCommandes.TraiterCommandeAsync(c));
Commande[] resultats = await Task.WhenAll(taches);
chrono.Stop();
ProcesseurCommandes.AfficherStatistiques(resultats.ToList(), chrono.Elapsed);
Console.WriteLine("\n\nAppuyez sur une touche pour quitter...");
Console.ReadKey();
}
}
}
Points clés à retenir
- async/await : Syntaxe moderne pour la programmation asynchrone
- Task<T> : Représente une opération asynchrone qui retourne une valeur
- Task.WhenAll : Exécuter plusieurs tâches en parallèle
- Task.WhenAny : Attendre la première tâche terminée
- CancellationToken : Permet d'annuler des opérations longues
- Ne jamais bloquer : Éviter .Result et .Wait(), toujours utiliser await
- async Task : Préférer async Task à async void
- Gestion d'erreurs : try/catch fonctionne naturellement avec await
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.