Multithreading in C#, Teil 25: AggregateException und UnobservedTaskException

In multi-threading Anwendungen gestaltet sich die Implementierung eines zuverlässigen Exception Handlings zumeist schwierig und ist mit einigem Aufwand verbunden. In diesem Artikel möchte ich Ihnen daher die grundlegenden Kenntnisse zum Exception Handling in Tasks vermitteln.

 
Exception Handling in Tasks

Tasks bieten eine komfortables Exception Handling. Exceptions müssen nicht umständlich innerhalb des Task Codes gefangen, behandelt und weiter gegeben werden. Sie werden stattdessen automatisch nach aussen geworfen. Ein Aufrufer, der mittels Wait Funktion oder Result Property auf das Ende des Tasks wartet, kann dies in einem Try-Catch Block ausführen und Task Exceptions dadurch fangen und behandeln. Diese werden als AggregateException geworfen. Der nachfolgende Quellcode zeigt ein grundlegendes Beispiel.

static void Main(string[] args)
{
    Task task = Task.Factory.StartNew(DoSomething);
            
    try
    {
        task.Wait();
    }
    catch (AggregateException exception)
    {
        Console.Write(exception.InnerException.Message);  // DivideByZeroException
    }

    Console.ReadKey();
}

static void DoSomething()
{
    int x = 0;
    int y = 10 / x;
}

 
AggregateException

Bei der parallelen Ausführung mehrere Programmteile können gleichzeitig mehre Ausnahmen in verschiedenen Threads auftreten. Daher wird bei Tasks immer eine AggregateException geworfen welche alle aufgetretenen Ausnahmen beinhaltet. Beispielsweise könnte ein Task mehrere Child Tasks haben, welche unterschiedliche Exceptions auslösen.

Die AggregateException bietet ein InnerExceptions Property welches alle aufgetretenen Fehler auflistet. Die inneren Ausnahmen können wiederum AggregateException sein wodurch eine Baumstruktur entsteht. Solch eine Baumstruktur auszuwerten ist nicht trivial. Das .NET Framework stellt aber mittels Flatten und Handle zwei Möglichkeiten bereit um diese Auswertung zu vereinfachen.

 
Flatten

Mittels der Flatten Funktion lässt sich die angesprochene Baumstruktur in eine flache Liste überführen. Dies ermöglicht einen simplen Loop über alle Exceptions.

Der nachfolgende Quellcode zeigt einen Task mit zwei Child Tasks welche jeweils eine Exception werfen. Im Try-Catch Block wird mittels der Flatten Funktion eine flache Liste aller Ausnahmen erzeugt welche anschliessend ausgewertet werden kann. Bitte beachten Sie dass die AggregateException in diesem Beispiel bereits eine flache Struktur aufweist da zur Vereinfachung des Beispiels keine weitere Task Verschachtelung  eingefügt wurde.

static void Main(string[] args)
{
    Task task = Task.Factory.StartNew(DoSomething);

    try
    {
        task.Wait();
    }
    catch (AggregateException exception)
    {
        foreach (Exception ex in exception.Flatten().InnerExceptions)
        {
            Console.WriteLine(ex.Message);
        }
    }

    Console.ReadKey();
}

static void DoSomething()
{
    int x = 0;

    Task.Factory.StartNew(() => 5 / x, TaskCreationOptions.AttachedToParent);
    Task.Factory.StartNew(() => { throw null; }, TaskCreationOptions.AttachedToParent);
}

 
Handle

Die AggregateException kann beliebige Strukturen und Inhalte aufweisen. Stellen Sie sich vor Sie wollen alle DivideByZeroException Fehler auffangen und behandeln. In diesem Fall wollen Sie sicher nicht den kompletten Baum nach allen Stellen untersuchen an denen diese Exception vorkommt. Dem Bedürfnis der einfachen Fehlerbehandlung kommt daher die Handle Funktion entgegen. Mittels dieser Funktion lassen sich spezifische Exceptions aus der AggregateException behandeln. Somit können gezielt einige Ausnahmen behandelt und der Rest erneut geworfen werden.

Der nachfolgende Quellcode zeigt das zuvor erstelle Beispiel, erweitert um ein Exception Handling mittels Handle Funktion. Dabei werden alle DivideByZeroException Fehler behandelt. Alle anderen Fehler werden erneut geworfen.

static void Main(string[] args)
{
    Task task = Task.Factory.StartNew(DoSomething);

    try
    {
        task.Wait();
    }
    catch (AggregateException exception)
    {
        exception.Flatten().Handle(ex =>  
        {
            if (ex is DivideByZeroException)
            {
                Console.WriteLine("DivideByZeroException");
                return true; //exception is handled
            }

            return false; //rethrow all other exceptions
        });
    }

    Console.ReadKey();
}

static void DoSomething()
{
    int x = 0;

    Task.Factory.StartNew(() => 5 / x, TaskCreationOptions.AttachedToParent);
    Task.Factory.StartNew(() => { throw null; }, TaskCreationOptions.AttachedToParent);
}

 
TaskScheduler.UnobservedTaskException

Unbehandelte Task Exceptions führen zum Abbruch der Applikation. Diese können aber mittels des TaskScheduler.UnobservedTaskException Events gefangen und behandelt werden. Der nachfolgende Quellcode zeigt ein entsprechendes Beispiel.

static void Main(string[] args)
{
    TaskScheduler.UnobservedTaskException += ExceptionHandler;

    Task task = Task.Factory.StartNew(DoSomething);

    Thread.Sleep(100);
    GC.Collect();
    GC.WaitForPendingFinalizers();

    Console.ReadKey();
}

static void DoSomething()
{
    int x = 0;
    int y = 10 / x;
}

static void ExceptionHandler(object sender, UnobservedTaskExceptionEventArgs eventArgs)
{
    Console.Write(((AggregateException)eventArgs.Exception).InnerException.Message);
}

 

Dabei gilt es zu beachten, dass die Fehler nicht sofort geworfen werden. Erst wenn der Task durch den Garbage Collector aufgeräumt wird und der Finalizer des Task aufgerufen wird, kommt es zum Auslösen des Fehlers. Zur Demonstration des Event Handlers wurde das Beispiel daher um die expliziten Aufrufe des Garbage Collectors erweitert. Beachten Sie beim Testen des Beispiels ausserdem, dass je nach Entwicklungsumgebung und Einstellungen das Event unter Umständen nur im Release Build aufgerufen wird.

 
Fazit

Die Ausnahmebehandlung in Tasks wird durch die Verwendung von AggregateException und den Funktionen Flatten und Handle stark vereinfacht. Diese Werkzeuge ermöglichen eine effiziente Implementierung des  Exception Handlings.

Werbung
Dieser Beitrag wurde unter .NET, C#, Multithreading abgelegt und mit , , , verschlagwortet. Setze ein Lesezeichen auf den Permalink.

Kommentar verfassen

Trage deine Daten unten ein oder klicke ein Icon um dich einzuloggen:

WordPress.com-Logo

Du kommentierst mit deinem WordPress.com-Konto. Abmelden /  Ändern )

Facebook-Foto

Du kommentierst mit deinem Facebook-Konto. Abmelden /  Ändern )

Verbinde mit %s