Montée en compétence auto-rythmée - C#
📚 Module 6 : Programmation Asynchrone
🔗 Prérequis : Module 1-5 ✅ (Fondamentaux, POO, Collections/LINQ, Gestion d'Erreurs, C# Moderne)
Objectifs
- Comprendre async/await et Task de base
- Savoir faire des opérations parallèles simples
- Éviter les pièges de base (deadlocks, exceptions)
- Identifier quand utiliser l'asynchrone
Contenu théorique
TasketTask<T>: création et attente- Async/await : syntaxe de base et propagation
- Task.Delay() : simulation d'opérations asynchrones
- Task.WhenAll : parallélisation simple
- Exception handling : try/catch avec async
- Deadlocks : comment les éviter
Exemple rapide
// Simulation d'une opération asynchrone
public class FileProcessor
{
public async Task<string> ProcessFileAsync(string filename)
{
Console.WriteLine($"Starting to process {filename}...");
await Task.Delay(2000); // 2 secondes
Console.WriteLine($"Finished processing {filename}");
return $"Processed content of {filename}";
}
// Traitement parallèle de plusieurs fichiers
public async Task<List<string>> ProcessMultipleFilesAsync(List<string> filenames)
{
Console.WriteLine($"Processing {filenames.Count} files in parallel...");
// Créer toutes les tâches
var tasks = filenames.Select(name => ProcessFileAsync(name));
// Attendre que toutes se terminent
var results = await Task.WhenAll(tasks);
return results.ToList();
}
}
public class Program
{
public static async Task Main(string[] args)
{
var processor = new FileProcessor();
// Traitement séquentiel
var result1 = await processor.ProcessFileAsync("file1.txt");
Console.WriteLine(result1);
// Traitement parallèle
var files = new List<string> { "file2.txt", "file3.txt", "file4.txt" };
var results = await processor.ProcessMultipleFilesAsync(files);
foreach (var result in results)
{
Console.WriteLine(result);
}
}
}
📖 Ressources théoriques
🎯 Projet fil rouge : TechEvent Manager - Phase 6
📋 Contexte
Phases précédentes :
- Phase 1-5 : Système d'événements moderne et robuste
Phase 6 : Les opérations externes (emails, API, notifications) bloquent le thread. Il faut rendre le système asynchrone pour améliorer performances et réactivité.
Objectif : Transformer TechEvent Manager en système async avec envoi d'emails, API externes, opérations parallèles.
💻 Exercice pratique : TechEvent Manager Asynchrone
Objectif : Ajouter async/await au système d'événements pour gérer opérations I/O.
Code de base à améliorer (version synchrone) :
namespace TechEventManager;
public class EmailService
{
public void SendConfirmationEmail(Participant participant, Event evt)
{
Console.WriteLine($"Sending email to {participant.Email}...");
Thread.Sleep(2000); // Simule appel SMTP
Console.WriteLine($"Email sent to {participant.Email}");
}
}
public class ExternalEventApi
{
public EventDetails FetchEventDetails(int eventId)
{
Console.WriteLine($"Fetching details for event {eventId}...");
Thread.Sleep(1500); // Simule appel API
return new EventDetails { Id = eventId, ExternalRating = 4.5 };
}
}
public class EventService
{
private readonly EventSearchEngine _searchEngine;
private readonly EmailService _emailService;
public bool RegisterParticipant(int eventId, Participant participant)
{
var evt = _searchEngine.FindEventById(eventId);
if (evt == null) throw new ArgumentException("Event not found");
var success = evt.Register(participant);
if (success)
{
// Bloque le thread pendant 2 secondes !
_emailService.SendConfirmationEmail(participant, evt);
}
return success;
}
public List<bool> RegisterMultipleParticipants(int eventId, List<Participant> participants)
{
var results = new List<bool>();
// Séquentiel - très lent si beaucoup de participants
foreach (var participant in participants)
{
var result = RegisterParticipant(eventId, participant);
results.Add(result);
}
return results;
}
}
public record EventDetails(int Id, double ExternalRating);
🎯 Missions à Implémenter
Mission 1 : Conversion Async des Services Externes
À implémenter :
public class EmailService
{
// TODO: Créer SendConfirmationEmailAsync(Participant, Event)
// Valider paramètres, logger, simuler avec Task.Delay(2000), logger fin
}
public class ExternalEventApi
{
// TODO: Créer FetchEventDetailsAsync(int eventId) → Task<EventDetails>
// Valider eventId, logger, simuler avec Task.Delay(1500), retourner EventDetails
}
Indice : Thread.Sleep → await Task.Delay, void → Task, T → Task<T>
Mission 2 : Registration Asynchrone avec Email
À implémenter :
public class EventService
{
private readonly EventSearchEngine _searchEngine;
private readonly EmailService _emailService;
// TODO: Créer RegisterParticipantAsync(int eventId, Participant) → Task<bool>
// Trouver événement, inscrire, si succès envoyer email async, retourner résultat
}
Indice : Propager async jusqu'à l'appelant (Main async).
Mission 3 : Inscriptions Parallèles
À implémenter :
public class EventService
{
// TODO: Créer RegisterMultipleParticipantsSequentialAsync (version séquentielle)
// Utiliser foreach avec await dans la boucle → lent mais simple
// TODO: Créer RegisterMultipleParticipantsAsync (version parallèle)
// Créer toutes les tasks, puis await Task.WhenAll(tasks) → rapide
}
Indice : Task.WhenAll exécute toutes les tâches en parallèle.
Mission 4 : Gestion d'Erreurs Simple
À implémenter :
public class EmailService
{
// TODO: Créer SendBulkEmailsAsync(List<Participant>, Event)
// foreach avec try/catch pour continuer même si un email échoue
// Compter sent et failed, logger résumé final
}
Indice : L'objectif est que si un email échoue, les autres continuent à s'envoyer.
Mission 5 : Programme de Démonstration
À implémenter :
// Demo console - Mettre tout ensemble
public class Program
{
public static async Task Main(string[] args)
{
Console.WriteLine("=== TechEvent Manager Async Demo ===\n");
// Initialiser les services
var emailService = new EmailService();
var api = new ExternalEventApi();
var searchEngine = new EventSearchEngine();
var eventService = new EventService(searchEngine, emailService);
// TODO: Demo 1 - Inscription simple avec email
// Créer workshop, participant, puis await RegisterParticipantAsync
// TODO: Demo 2 - Comparer séquentiel vs parallèle
// Utiliser Stopwatch pour comparer Sequential vs Parallel avec 3+ participants
// TODO: Demo 3 - Envoi bulk avec gestion d'erreurs
// Créer liste avec 1 email contenant "error", tester SendBulkEmailsAsync
}
}
Indice : Utiliser var sw = Stopwatch.StartNew(); puis sw.Stop(); et sw.ElapsedMilliseconds.
🧪 Tests unitaires
Objectif : Apprendre à tester les méthodes asynchrones avec async/await.
Format : 2 exemples fournis + 4 à implémenter vous-même
📝 Exemples fournis (2 tests)
Voici 2 exemples complets pour vous montrer comment tester async/await :
[Fact]
public async Task SendConfirmationEmailAsync_WithValidInputs_CompletesSuccessfully()
{
// Arrange
var emailService = new EmailService();
var participant = new Participant("John", "john@test.com");
var evt = new Workshop(1, "Test", DateTime.Now.AddDays(1), 20, "Paris", "Beginner", 2);
// Act
await emailService.SendConfirmationEmailAsync(participant, evt);
// Assert
// Pas d'exception = succès
Assert.True(true);
}
[Fact]
public async Task SendConfirmationEmailAsync_NullParticipant_ThrowsArgumentNullException()
{
// Arrange
var emailService = new EmailService();
var evt = new Workshop(1, "Test", DateTime.Now.AddDays(1), 20, "Paris", "Beginner", 2);
// Act & Assert
await Assert.ThrowsAsync<ArgumentNullException>(() =>
emailService.SendConfirmationEmailAsync(null, evt));
}
✍️ Tests à implémenter vous-même (4 tests)
En vous inspirant des exemples, implémentez ces 4 tests pour valider async/await :
Test 3 : SendBulkEmailsAsync - Gestion d'erreurs
[Fact]
public async Task SendBulkEmailsAsync_WithErrors_ContinuesProcessing()
{
// TODO: Implémenter ce test
// Créer un EmailService et un événement
// Créer une liste avec 3 participants
// Appeler SendBulkEmailsAsync (qui peut avoir des erreurs)
// Vérifier qu'aucune exception n'est levée (gestion d'erreurs avec try/catch)
}
Test 4 : RegisterMultipleParticipantsAsync - Exécution parallèle
[Fact]
public async Task RegisterMultipleParticipantsAsync_ParallelExecution_Works()
{
// TODO: Implémenter ce test
// Créer EventService avec plusieurs participants
// Mesurer le temps d'exécution de RegisterMultipleParticipantsAsync
// Vérifier que tous les participants sont inscrits correctement
// Bonus: Comparer avec version séquentielle pour vérifier gain de performance
}
Test 5 : GetEventStatisticsAsync - Combinaison de résultats
[Fact]
public async Task GetEventStatisticsAsync_CombinesResults_Correctly()
{
// TODO: Implémenter ce test
// Créer un événement avec plusieurs participants
// Appeler GetEventStatisticsAsync qui combine plusieurs opérations async
// Vérifier que les statistiques sont correctes (nombre d'inscrits, taux de remplissage)
}
Test 6 : RegisterParticipantAsync - Délai d'attente
[Fact]
public async Task RegisterParticipantAsync_CompletesWithinTimeout()
{
// TODO: Implémenter ce test
// Créer un EventService
// Mesurer le temps d'exécution avec Stopwatch
// Vérifier que l'opération se termine en moins de 3 secondes
// (l'envoi d'email simule 2 secondes)
}
Indice : Pour tester async, utilisez async Task comme type de retour et await Assert.ThrowsAsync<>() pour les exceptions.
🎓 Bonnes pratiques
Async all the way
// ❌ MAUVAIS : Bloquer avec .Result
public void RegisterUser(int eventId, Participant p)
{
var result = RegisterParticipantAsync(eventId, p).Result; // Deadlock risk!
}
// ✅ BON : Propager async
public async Task RegisterUserAsync(int eventId, Participant p)
{
await RegisterParticipantAsync(eventId, p);
}
Task.WhenAll pour parallélisme
// ❌ MAUVAIS : Await dans boucle = séquentiel
foreach (var p in participants)
{
await SendEmailAsync(p); // Un par un
}
// ✅ BON : Task.WhenAll = parallèle
var tasks = participants.Select(p => SendEmailAsync(p));
await Task.WhenAll(tasks);
🎯 Questions d'entretien
1. Différence entre Task.Wait() et await ?
Voir la réponse
await libère le thread pendant l'attente (non-bloquant). Wait() bloque le thread (risque de deadlock). Toujours préférer await.
// ❌ MAUVAIS : Wait() bloque le thread
public void RegisterUser(int eventId, Participant p)
{
var task = RegisterParticipantAsync(eventId, p);
task.Wait(); // Bloque le thread ! Risque deadlock en UI/ASP.NET
}
// ✅ BON : await libère le thread
public async Task RegisterUserAsync(int eventId, Participant p)
{
await RegisterParticipantAsync(eventId, p); // Thread libre pendant l'attente
}
// Deadlock classique avec Wait()
public void DeadlockExample()
{
// Dans un contexte UI ou ASP.NET
var result = GetDataAsync().Result; // Deadlock !
// GetDataAsync attend le contexte UI qui est bloqué par Result
}
// Solution
public async Task NoDeadlockExample()
{
var result = await GetDataAsync(); // Pas de deadlock
}
2. Comment faire des opérations en parallèle ?
Voir la réponse
Créer toutes les tâches sans await : var tasks = items.Select(x => DoAsync(x)); puis await Task.WhenAll(tasks); pour les exécuter en parallèle.
// ❌ MAUVAIS : Séquentiel (une par une)
foreach (var participant in participants)
{
await SendEmailAsync(participant); // Attend chacune
// Total: 3 participants × 2s = 6 secondes
}
// ✅ BON : Parallèle avec Task.WhenAll
var tasks = participants.Select(p => SendEmailAsync(p));
await Task.WhenAll(tasks);
// Total: 2 secondes (toutes en même temps)
// Exemple complet
public async Task<List<EventDetails>> FetchMultipleEventsAsync(List<int> eventIds)
{
// Créer toutes les tâches SANS await
var tasks = eventIds.Select(id => FetchEventDetailsAsync(id));
// Attendre que toutes se terminent
var results = await Task.WhenAll(tasks);
return results.ToList();
}
// Avec gestion d'erreurs
try
{
await Task.WhenAll(tasks);
}
catch (Exception ex)
{
// Première exception seulement
// Toutes les exceptions dans aggregateException.InnerExceptions
}
3. Que se passe-t-il si exception dans méthode async ?
Voir la réponse
L'exception est stockée dans la Task, puis relancée lors du await. Avec Task.WhenAll, la première exception est relancée mais toutes sont capturées dans task.Exception.
// Exception dans méthode async
public async Task<Event> GetEventAsync(int id)
{
await Task.Delay(100);
throw new EventNotFoundException(id); // Exception stockée dans Task
}
// Gestion de l'exception
try
{
var evt = await GetEventAsync(123); // Exception relancée ici
}
catch (EventNotFoundException ex)
{
Console.WriteLine($"Event not found: {ex.EventId}");
}
// Avec Task.WhenAll
var tasks = new[]
{
SendEmailAsync(p1), // Réussit
SendEmailAsync(p2), // Lance InvalidEmailException
SendEmailAsync(p3) // Lance TimeoutException
};
try
{
await Task.WhenAll(tasks);
}
catch (Exception ex)
{
// ex = InvalidEmailException (première exception)
// Accéder à TOUTES les exceptions
var aggregateTask = Task.WhenAll(tasks);
try { await aggregateTask; }
catch
{
foreach (var innerEx in aggregateTask.Exception.InnerExceptions)
{
Console.WriteLine($"Error: {innerEx.Message}");
}
}
}
4. Pourquoi éviter .Result ou .Wait() ?
Voir la réponse
.Result et .Wait() bloquent le thread, créent un risque de deadlock (surtout en UI/ASP.NET), et causent une perte de stack trace. Utiliser async all the way.
// ❌ MAUVAIS : Utilisation de .Result
public Event GetEvent(int id)
{
// Bloque le thread, risque deadlock
return GetEventAsync(id).Result;
}
// ❌ MAUVAIS : Utilisation de .Wait()
public void RegisterUser(Participant p)
{
var task = RegisterParticipantAsync(p);
task.Wait(); // Bloque le thread
}
// ✅ BON : Async all the way
public async Task<Event> GetEventAsync(int id)
{
return await FetchEventFromDatabaseAsync(id);
}
public async Task RegisterUserAsync(Participant p)
{
await RegisterParticipantAsync(p);
}
// Problème de deadlock en ASP.NET
// Controller ASP.NET Core
public IActionResult GetData()
{
// ❌ Deadlock !
var data = GetDataAsync().Result;
return Ok(data);
}
// ✅ Solution
public async Task<IActionResult> GetDataAsync()
{
var data = await GetDataAsync();
return Ok(data);
}
5. Quand utiliser Task.Delay vs Thread.Sleep ?
Voir la réponse
Task.Delay est async et libère le thread. Thread.Sleep est synchrone et bloque le thread. Toujours utiliser Task.Delay dans du code async.
// ❌ MAUVAIS : Thread.Sleep bloque le thread
public async Task SendEmailAsync(Participant p)
{
Console.WriteLine("Sending email...");
Thread.Sleep(2000); // ❌ Bloque le thread pendant 2 secondes !
Console.WriteLine("Email sent");
}
// ✅ BON : Task.Delay libère le thread
public async Task SendEmailAsync(Participant p)
{
Console.WriteLine("Sending email...");
await Task.Delay(2000); // ✅ Thread libre pendant 2 secondes
Console.WriteLine("Email sent");
}
// Exemple concret de différence
public async Task ProcessMultipleFilesAsync()
{
var tasks = new[]
{
ProcessWithThreadSleep(), // Bloque 3 threads
ProcessWithThreadSleep(),
ProcessWithThreadSleep()
};
await Task.WhenAll(tasks); // 3 secondes, 3 threads bloqués
}
public async Task ProcessWithThreadSleep()
{
Thread.Sleep(3000); // Thread bloqué
}
// vs
public async Task ProcessMultipleFilesOptimizedAsync()
{
var tasks = new[]
{
ProcessWithTaskDelay(), // Libère les threads
ProcessWithTaskDelay(),
ProcessWithTaskDelay()
};
await Task.WhenAll(tasks); // 3 secondes, 0 threads bloqués
}
public async Task ProcessWithTaskDelay()
{
await Task.Delay(3000); // Thread libre
}
✅ Validation du module
Checklist TechEvent Manager - Phase 6 :
- Mission 1 : EmailService.SendConfirmationEmailAsync() implémentée avec await Task.Delay(2000)
- Mission 1 : ExternalEventApi.FetchEventDetailsAsync() implémentée avec await Task.Delay(1500)
- Mission 2 : EventService.RegisterParticipantAsync() qui envoie un email async
- Mission 3 : RegisterMultipleParticipantsSequentialAsync() avec foreach + await
- Mission 3 : RegisterMultipleParticipantsAsync() avec Task.WhenAll
- Mission 4 : SendBulkEmailsAsync() avec try/catch pour gérer les erreurs
- Mission 5 : Programme Main avec 3 démos fonctionnelles
- Je constate que la version parallèle est plus rapide (Stopwatch)
- Aucun .Result ou .Wait() dans mon code
- Je réponds correctement aux 5 questions d'entretien
⬅️ Module 5 - C# Moderne | Retour au MECA | Module 7 - Test avancé ➡️