Exceptions non gérées en TPL avec .NET 4.0 vs .NET 4.5
Si vous travaillez avec .NET 4.0 et qu’une tâche (Task) est dans un état d’échec (une exception a été lancée dans le corps de la tache sans être gérée), vous devez inspecter (observer) l’erreur sinon votre application risque de se terminer brutalement dans un futur proche sans vous informer clairement sur la nature du problème. Cela peut paraître étrange, mais l’équipe TLP avait décidé de rendre obligatoire « l’observation » même si les conséquences étaient catastrophiques.
Pour expliquer le phénomène, prenons le cas concret d’une tâche qui lève une exception sans être gérée dans la tâche elle-même. Si la tâche n’a plus de référence sur aucune variable, elle terminera sa vie à travers le processus de finalisation, comme tous les objets possédant une méthode Finalize. En .NET 4.0, lorsque le thread de finalisation tente de finaliser une tache non observée, l’exception originale est levée, puis relancée depuis ce thread.
Vous l’avez compris, c’est le Garbage Collector (GC) qui engendre l’exception. Mais par nature le GC s’exécute si besoin (de mémoire). Il peut même arriver que le développeur ne détecte pas l’exception. Comme le traitement est différé et cela rend très difficile le diagnostic, car parfois le problème se révèle uniquement en production. Ce phénomène a fait couler beaucoup d’encre dans les forums .NET.
class Program
{
Task.Factory.StartNew(() =>
{
throw new ArgumentException();
});
Task.Factory.StartNew(() =>
{
throw new InvalidCastException();
});
Thread.Sleep(100);
GC.Collect();
GC.WaitForPendingFinalizers();
Console.WriteLine("Finished");
Console.ReadKey();
}
}
En .NET 4.5, l’équipe TPL a entendu les plaintes des développeurs face à ce phénomène et elle a décidé de modifier le comportement par défaut, en ne relançant plus l’exception de la tâche au sein du thread de finalisation. En d’autres mots les exceptions des tâches non observées seront étouffées. Naturellement, si vous souhaiter être compatible .NET 4.0, vous pouvez ajouter dans votre fichier de configuration les lignes suivantes.
<configuration>
<runtime>
<ThrowUnobservedTaskException enabled="true" />
</runtime>
</configuration>
Naturellement, cette modification ne doit pas nous pousser à écrite du code qui ne gère pas leurs exceptions. La façon la plus simple est traiter les exceptions non observer en utilisant un évènement UnobservedTaskException statique du type TaskScheduler.
public static void Main()
{
TaskScheduler.UnobservedTaskException += (sender, eventArgs) =>
{
eventArgs.SetObserved();
eventArgs.Exception.Handle(ex =>
{
Console.WriteLine("Exception type: {0}", ex.GetType());
return true;
});
};
Task.Factory.StartNew(() =>
{
throw new ArgumentException();
});
Task.Factory.StartNew(() =>
{
throw new InvalidCastException();
});
Thread.Sleep(100);
GC.Collect();
GC.WaitForPendingFinalizers();
Console.WriteLine("Finished");
Console.ReadKey();
}
À bientôt
Bruno