This article is available in english.
Lorsque l’on développe des applications .NET, les exceptions non gérées dans des threads ont le désagréable effet de terminer le processus courant.
Dans l’exemple suivant :
1 2 3 4 5 6 7 8 9 10 11 12 | static void Main(string[] args) { var t = new Thread(ThreadMethod); t.Start();
Console.ReadLine(); }
private static void ThreadMethod() { Thread.Sleep(1000); throw new Exception(); } |
Cette exception très simple va invariablement terminer le processus, et pour empêcher cela, l’exception doit être gérée correctement :
1 2 3 4 5 6 7 8 9 10 11 | private static void ThreadMethod() { try { Thread.Sleep(1000); throw new Exception(); } catch (Exception e) { // TODO: Log and report the exception } } |
Cela rend des classes comme System.Threading.Thread, System.Threading.Timer ou System.Threading.ThreadPool très dangereuses à utiliser si l’on veut une application toujours en fonctionnement. Il est alors requis qu’aucune exception ne sorte non gérée d’un des handlers donnés à une de ces classes.
Même si il est possible, en utilisant l’évènement AppDomain.UnhandledException, d’être notifié lorsqu’une exception a été levée et n’a pas été gérée proprement, la plupart du temps cela mène à l’arrêt de l’application. Ce comportement a été introduit en .NET 2.0, afin d’éviter que les exceptions non gérées soit ignorées silencieusement.
Ceci étant, bien que cela soit un comportement par défaut tout à fait approprié, dans un environnement d’entreprise, je fais en sorte d’imposer des règles d’analyse statique de code (via VS2010 ou NDepend) qui empêchent l’utilisation en direct de ces classes. Cela permet de forcer l’utilisation de classes wrappers qui interceptent les exceptions avec un filtre très large afin de les logger et de les rapporter, mais qui ne termine pas le processus.
Bien entendu, cela dénote la présence d’un véritable bug qui doit être analysé, puisque les exceptions ne doivent pas être gérées aussi tard.
Le cas du Reactive Framework
Dans Silverlight pour Windows Phone 7, et dans n’importe quelle application .NET 3.5 or .NET 4.0 qui utilise les Reactive Extensions, il est très simple de changer de threads.
Les opérateurs Réactifs comme Timer, BufferWithTime, ObserveOn ou SubscribeOn permettent l’utilisation de Schedulers spécifiques comme ThreadPool, TaskPool ou NewThread, et si un subscriber ne gère pas l’exception proprement, le processus se terminera aussitôt.
Cet exemple adapté au Reactive Framework termine l’application :
1 2 3 4 5 6 7 8 9 10 11 12 | static void Main(string[] args) { Observable.Timer(TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(10)) .Subscribe(_=> ThreadMethod());
Console.ReadLine(); }
private static void ThreadMethod() { throw new Exception(); } |
L’opérateur Observable.Timer utilise la classe System.Threading.Timer et cela la rend autant vulnérable aux problèmes de terminaison. Chaque subscriber doit gérer les exceptions lancées dans le delegate OnNext, ou l’application se terminera.
Aussi, ne pensez pas que le delegate OnError passé à la méthode Observable.Subscribe va gérer les exceptions levées durant l’execution du code dans OnNext. OnError va simplement notifier des erreurs levées durant l’execution des operateurs précédents, et non pas le courant.
La Méthode d’extension IScheduler.AsSafe()
Malheureusement, il n’est pour l’instant pas possible de surcharger le scheduler par défaut utilisé en interne par les opérateurs Réactifs. Le seul moyen de gérer toutes les exceptions non gérées est d’utiliser l’opérateur ObserveOn, et d’intercepter les appels à la méthode IScheduler.Schedule. Les appels peuvent alors être décorés avec des gestionnaires d’exceptions qui loggent et raportent l’exception, sans terminer le processus.
Donc, pour généraliser ce comportement de logging et de rapport d’erreur, j’ai donc créé la méthode d’extension AsSafe() que l’on peut placer tout en haut d’une expression Réactive :
1 2 3 | Observable.Timer(TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(10)) .ObserveOn(Scheduler.ThreadPool.AsSafe()) .Subscribe(_=> ThreadMethod()); |
Et voici le code de cette très simple extension :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | public static class SafeSchedulerExtensions { public static IScheduler AsSafe(this IScheduler scheduler) { return new SafeScheduler(scheduler); }
private class SafeScheduler : IScheduler { private IScheduler _source;
public SafeScheduler(IScheduler scheduler) { this._source = scheduler; }
public DateTimeOffset Now { get { return _source.Now; } }
public IDisposable Schedule(Action action, TimeSpan dueTime) { return _source.Schedule(Wrap(action), dueTime); }
public IDisposable Schedule(Action action) { return _source.Schedule(Wrap(action)); }
private Action Wrap(Action action) { return () => { try { action(); } catch (Exception e) { // Log and report the exception. } };
} } } |