Avez-vous déjà écrit une boucle dans votre code qui semble ne jamais devoir se terminer ? C’est un scénario courant dans les logiciels conçus pour fonctionner en continu, comme les services qui s’exécutent en arrière-plan. Par exemple, un service web écoute généralement sur une adresse et un port spécifiques, attendant indéfiniment des connexions entrantes pour accomplir sa tâche.
Puisque ces connexions peuvent arriver à tout moment, le service doit toujours être prêt, ce qui implique souvent l'utilisation d'une boucle infinie.
Voici un exemple de base en C# :
while (true)
{
if (Listener.Pending())
{
ISocketWrapper client = new SocketWrapper(Listener.AcceptSocket());
await AcceptConnectionsAsync(client);
}
}
Dans cette boucle, le service vérifie si une nouvelle connexion est en attente. Si c'est le cas, il accepte la connexion et traite le client. Sinon, la boucle continue, répétant indéfiniment la même vérification.
À première vue, une boucle infinie semble être une solution raisonnable — après tout, le service doit toujours fonctionner. Et si vous avez besoin de stopper le service, vous pouvez simplement terminer le processus. Bien que cela puisse ne pas toujours poser de problème, ce n'est ni la solution la plus élégante ni la plus sûre.
Le Problème des Boucles Infinies
Même si une boucle infinie peut sembler acceptable pour le moment, que se passe-t-il lorsque le service doit évoluer ? Par exemple, que faire si vous devez mettre à jour l'adresse ou le port d'écoute ? Redémarrer l'intégralité du service pourrait fonctionner, mais cela est souvent peu pratique et peut interrompre d'autres opérations en cours. Pire encore, forcer l'arrêt d'un service peut entraîner des pertes de données ou d'autres conséquences non désirées, surtout si le service est en plein traitement d'une requête client.
C'est ici qu'une meilleure solution entre en jeu : nous avons besoin d'un moyen de sortir de la boucle de manière élégante, sans interrompre brusquement le processus, et qui peut être appelé de n’importe où. C’est là qu’intervient le Cancellation Token.
Qu’est-ce qu’un Cancellation Token ?
Un Cancellation Token est un objet en C# qui permet de signaler une demande d'arrêt pour une tâche. En l’utilisant dans votre code, vous pouvez implémenter une condition de sortie contrôlée pour vos boucles infinies, garantissant que lorsque survient une demande d'arrêt, le service ait la possibilité de terminer proprement son travail en cours.
Améliorons l'exemple précédent en intégrant un CancellationToken
:
public async Task StartListeningAsync(CancellationToken token)
{
while (!token.IsCancellationRequested)
{
if (Listener.Pending())
{
ISocketWrapper client = new SocketWrapper(Listener.AcceptSocket());
await AcceptConnectionsAsync(client);
}
}
// Cleaning work
}
Désormais, au lieu de boucler indéfiniment, la boucle continue tant que le CancellationToken
ne signale pas une demande d'annulation. Ce token permet une fermeture contrôlée du service lorsqu'il est nécessaire de l'arrêter.
Un Cancellation Token c’est bien, deux c’est mieux...
En effet, une application complexe peut avoir plusieurs tâches liées ou non entre elles. Quand une tâche parent doit être annulée, il faut être capable d’annuler également les tâches enfants. Sinon, on est contraint d’attendre que la tâche enfant se termine, ce qui peut être long.
Plusieurs stratégies existent pour cela. La plus simple est de passer votre Cancellation Token aux différentes sous-tâches. Par exemple, une légère modification de la signature de AcceptConnectionsAsync
suffirait :
await AcceptConnectionsAsync(client, token);
Si toutes les tâches enfants sont toujours annulées avec la tâche parent, ce modèle est suffisant. Cependant, il ne permet pas d’annuler une tâche enfant sans annuler la tâche parente.
Une Liste ou un Graphe de Tokens
Un bon moyen de contourner ce problème est d’avoir un Cancellation Token unique pour chaque tâche ou module, afin d’obtenir plus de contrôle et de granularité dans l’arrêt des tâches. Chaque token serait stocké dans une structure de données telle qu’une liste ou un graphe, selon vos besoins. L’annulation d’un module et de son token pourrait alors se propager aux sous-tâches associées.
Il existe plusieurs moyens simples d’implémenter ce comportement. Par exemple, vous pouvez utiliser un dictionnaire où la clé est le nom de la tâche ou du module, et l’élément est le token associé. Cela permet de gérer et d’annuler les tâches de manière organisée. Voici un exemple d'implémentation en C# :
public class WebServer
{
public Dictionary<string, CancellationTokenSource> CancellationTokens = new Dictionary<string, CancellationTokenSource>();
public readonly CancellationTokenSource GlobalTokenSource = new CancellationTokenSource();
public async Task StopAsync(bool isRestart = false)
{
foreach (var tokenName in CancellationTokens.Keys)
{
Console.WriteLine($"Cancelling token {tokenName}: {CancellationTokens[tokenName].Token}");
CancellationTokens[tokenName].Cancel();
}
// Wait if needed for tasks to finish
await Task.Delay(1000); // Simulate wait
}
}
Dans cet exemple, la classe WebServer
utilise un dictionnaire pour stocker les Cancellation Tokens associés à différentes tâches ou modules. La méthode StopAsync
parcourt les clés du dictionnaire, annule chaque token, et permet de s’assurer que toutes les tâches sont correctement stoppées, tout en offrant la flexibilité nécessaire pour gérer les tâches individuellement ou en groupe.
Conclusion
Avoir envie de mettre en place une boucle infinie dans votre code doit désormais être un red flag. Avant de continuer, il est essentiel de vous demander si un Cancellation Token ne serait pas plus approprié pour gérer cette boucle de manière élégante. En intégrant ces tokens, vous vous assurez de pouvoir arrêter vos services proprement, sans compromettre leur stabilité et sans interrompre les traitements en cours de manière brusque.
Merci d'avoir pris le temps de lire cet article ! J'espère qu'il vous a aidé à mieux comprendre comment gérer efficacement les boucles infinies et les Cancellation Tokens en C#.
N'hésitez pas à laisser un commentaire si vous avez des questions ou des idées à partager.
Si vous souhaitez en apprendre davantage sur C# et son environnment, venez me rejoindre sur Twitch! J'y partage régulièrement des sessions de programmation en direct sur la programmation.