Service Windows avec démarrage asynchrone

Le numéro de novembre dernier de MSDN Magazine contient un article de Mark Sowul intitulé Asynchronous Programming – Async from the Start. Il y est expliqué de façon très pédagogique comment démarrer une application WinForms ou WPF de manière asynchrone sans écueil. Cela m’a donné l’idée d’appliquer exactement le même sujet sur un service Windows (un hôte .NET).

Cela peut être pratique, notamment quand l’initialisation du service peut être longue. L’intérêt peut sembler mineur pour un programme dépourvu d’interface graphique. Il existe cependant des situations où une initialisation virtuellement rapide est intéressante. Je pense notamment au déploiement automatique. Si celui-ci démarre le service, il attendra généralement le bon démarrage. Si le démarrage est long, au mieux cela rallonge de manière systématique la durée du déploiement, au pire le délai d’attente est dépassé et le déploiement échoue de façon inappropriée alors que le service est encore en cours de démarrage. Une initialisation asynchrone entraîne un démarrage virtuellement rapide du point de vue du Service Control Manager.

Noter néanmoins qu’il est toujours conseillé de respecter un délai raisonnable (et le plus court possible) lors du démarrage et a fortiori l’arrêt d’un service, puisque celui-ci peut retarder l’arrêt du système.

La difficulté vient du fait que ni le point d’entrée du programme, ni la classe native ServiceBase ne prévoient un démarrage asynchrone.

Or, le framework TPL est conçu avec comme contrainte de créer une chaîne de fonctions asynchrones, du début à la fin. C’est la raison pour laquelle il n’est pas supporté par cette TPL d’exécuter une méthode asynchrone… de manière synchrone. Dit comme cela, c’est évident. Par voie de conséquence, on ne peut pas invoquer du code asynchrone depuis un code synchrone. Ce qui est une réelle limitation. Cela reste possible, mais ce n’est pas simple et pas recommandé dans les guidelines de la TPL.

Dans le cas du démarrage du service, il nous faut gérer les exceptions dans la phase d’initialisation asynchrone, ainsi que l’arrêt propre du service, une fois l’initialisation accomplie.

L’astuce est d’inscrire une tâche de continuation à la tâche qui gère l’initialisation (le démarrage) du service. En cas d’exception dans la tâche d’initialisation, la TPL est conçue pour propager celle-ci dans sa tâche de continuation.

La tâche de continuation a deux responsabilités:

  • En cas d’erreur, déclencher l’arrêt du service.
  • Dans le cas contraire, bloquer jusqu’à l’arrêt du service.

Lorsque l’arrêt du service est déclenché, un signal est émis pour débloquer la tâche de continuation. C’est elle qui invoque finalement l’arrêt sur son propre thread. Un signal est émis à la fin de celui-ci, et ce signal est simplement attendu par la méthode qui a déclenché le signal d’arrêt.

J’ai placé un exemple complet sur GitHub.

Je ne reprendrai pas ici les explications déjà détaillées dans l’article de MSDN Magazine que je vous conseille de lire (ainsi qu’un autre article qu’il référence, de Stephen Cleary). Je pense que l’analogie est évidente: lorsque l’article parle de la classe Form, pensez ServiceBase. Pour le reste, le fonctionnement est plus simple avec un service car il n’y a pas d’interface graphique à gérer (sauf cas très particuliers), et donc pas d’implémentation spéciale de SynchronizationContext.

L’exemple sur GitHub utilise également un conteneur IoC (SimpleInjector) responsable d’instancier notre application (la classe SampleHost). Celle-ci est bien découplée de l’hôte ServiceBase, comme le conseille le même article de MSDN Magazine. En cas d’arrêt et redémarrage du service, notre application est bien recréée. Nous avons en effet bien séparé le cycle de vie de notre application de celle du wrapper ServiceBase (lié au processus). Cela facilite non seulement les tests unitaires de l’application (qui ne dépend pas de ServiceBase) mais respecte également les fonctionnalités de démarrage et arrêt d’un service Windows: le redémarrage du service revient strictement au même qu’arrêter et relancer le processus. Je vois très souvent du code qui ne respecte pas cela.