Übersetzung vorschlagen
 
Andere Vorschläge:

progress indicator
Keine anderen Vorschläge
MSDN Magazin > Home > Ausgaben > 2009 > MSDN Magazin August 2009 >  .NET Themen: Aggregieren von Ausnahmen
Inhalt anzeigen:  Englisch mit deutscher ÜbersetzungInhalt anzeigen: Englisch mit deutscher Übersetzung
Dies sind maschinell übersetzte Inhalte, die von den Mitgliedern der Community bearbeitet werden können. Sie können die Übersetzung verbessern, indem Sie auf den jeweils zum Satz gehörenden Link "Bearbeiten" klicken.Mithilfe des Dropdown-Steuerelements "Inhalt anzeigen" links oben auf der Seite können Sie zudem bestimmen, ob nur der englische Originaltext, nur die deutsche Übersetzung oder beides nebeneinander angezeigt werden.
.NET Matters
Aggregating Exceptions
Stephen Toub
Exceptions in .NET are the fundamental mechanism by which errors and other exceptional conditions are communicated. Exceptions are used not only to store information about such issues but also to propagate that information in object-instance form through a call stack. Based on the Windows structured exception handling (SEH) model, only one .NET exception can be "in flight" at any particular time on any particular thread, a restriction that is usually nary a thought in developers' minds. After all, one operation typically yields only one exception, and thus in the sequential code we write most of the time, we need to be concerned about only one exception at a time. However, there are a variety of scenarios in which multiple exceptions might result from one operation. This includes (but is not limited to) scenarios involving parallelism and concurrency.
Consider the raising of an event in .NET:
public event EventHandler MyEvent;

protected void OnMyEvent() {
    EventHandler handler = MyEvent; 
    if (handler != null) handler(this, EventArgs.Empty); 
}
Multiple delegates can be registered with MyEvent, and when the handler delegate in the previous code snippet is invoked, the operation is equivalent to code like the following:
foreach(var d in handler.GetInvocationList()) {
    ((EventHandler)d)(this, EventArgs.Empty); 
}
Each delegate that makes up the handler multicast delegate is invoked one after the other. However, if any exception is thrown from any of the invocations, the foreach loop ceases processing, which means that some delegates might not be executed in the case of an exception. As an example, consider the following code:
MyEvent += (s, e) => Console.WriteLine("1");
MyEvent += (s, e) => Console.WriteLine("2");
MyEvent += (s, e) => { throw new Exception("uh oh"); };
MyEvent += (s, e) => Console.WriteLine("3");
MyEvent += (s, e) => Console.WriteLine("4");
If MyEvent is invoked now, "1" and "2" are output to the console, an exception is thrown, and the delegates that would have output "3" and "4" will not be invoked.
To ensure that all the delegates are invoked even in the face of an exception, we can rewrite our OnMyEvent method as follows:
protected void OnMyEvent() {
    EventHandler handler = MyEvent; 
    if (handler != null) { 
        foreach (var d in handler.GetInvocationList()) { 
            try { 
                ((EventHandler)d)(this, EventArgs.Empty); 
            } 
            catch{}
        } 
    } 
}
Now we're catching all the exceptions that escape from a registered handler, allowing delegates that come after an exception to still be invoked. If you rerun the earlier example, you'll see that "1", "2", "3", and "4" are output, even though an exception is thrown from one of the delegates. Unfortunately, this new implementation is also eating the exceptions, a practice that is greatly frowned on. Those exceptions could indicate a serious issue with the application, an issue that is not being handled because the exceptions are ignored.
What we really want is to capture any exceptions that might emerge, and then once we've finished invoking all the event handlers, throw again all the exceptions that escaped from a handler. Of course, as already mentioned, only one exception instance can be thrown on a given thread at a given time. Enter AggregateException.
In the .NET Framework 4, System.AggregateException is a new exception type in mscorlib. While a relatively simple type, it enables a plethora of scenarios by providing central and useful exception-related functionality.
AggregateException is itself an exception (deriving from System.Exception) that contains other exceptions. The base System.Exception class already has the notion of wrapping a single Exception instance, referred to as the "inner exception." The inner exception, exposed through the InnerException property on Exception, represents the cause of the exception and is often used by frameworks that layer functionality and that use exceptions to elevate the information being provided. For example, a component that parses input data from a stream might encounter an IOException while reading from the stream. It might then create a CustomParserException that wraps the IOException instance as the InnerException, providing higher-level details about what went wrong in the parse operation while still providing the IOException for the lower-level and underlying details.
AggregateException simply extends that support to enable wrapping of inner exceptions—plural. It provides constructors that accept params arrays or enumerables of these inner exceptions (in addition to the standard constructor that accepts a single inner exception), and it exposes the inner exceptions through an InnerExceptions property (in addition to the InnerException property from the base class). See Figure 1 for an overview of AggregateException's public surface area.
[Serializable]
[DebuggerDisplay("Count = {InnerExceptions.Count}")]
public class AggregateException : Exception
{
    public AggregateException();
    public AggregateException(params Exception[] innerExceptions);
    public AggregateException(IEnumerable<Exception> innerExceptions);
    public AggregateException(string message);
    public AggregateException(string message, Exception innerException);
    public AggregateException(string message, 
        params Exception[] innerExceptions);
    public AggregateException(string message, 
        IEnumerable<Exception> innerExceptions);

    public AggregateException Flatten();
    public void Handle(Func<Exception, bool> predicate);

    public ReadOnlyCollection<Exception> InnerExceptions { get; }
} 
If the AggregateException doesn't have any inner exceptions, InnerException returns null and InnerExceptions returns an empty collection. If the AggregateException is provided with a single exception to wrap, InnerException returns that instance (as you'd expect), and InnerExceptions returns a collection with just that one exception. And if the AggregateException is provided with multiple exceptions to wrap, InnerExceptions returns all those in the collection, and InnerException returns the first item from that collection.
Now, with AggregateException, we can augment our .NET event-raising code as shown in Figure 2, and we're able to have our cake and eat it, too. Delegates registered with the event continue to run even if one throws an exception, and yet we don't lose any of the exceptional information because they're all wrapped into an aggregate and thrown again at the end (only if any of the delegates fail, of course).
protected void OnMyEvent() { 
    EventHandler handler = MyEvent; 
    if (handler != null) { 
        List<Exception> exceptions = null;
        foreach (var d in handler.GetInvocationList()) 
        { 
            try { 
                ((EventHandler)d)(this, EventArgs.Empty); 
            } 
            catch (Exception exc) { 
                if (exceptions == null) 
                    exceptions = new List<Exception>();
                exceptions.Add(exc);
            } 
        } 
        if (exceptions != null) throw new AggregateException(exceptions); 
    } 
}
Events provide a solid example of where exception aggregation is useful for sequential code. However, AggregateException is also of prime importance for the new parallelism constructs in .NET 4 (and, in fact, even though AggregateException is useful for non-parallel code, the type was created and added to the .NET Framework by the Parallel Computing Platform team at Microsoft).
Consider the new Parallel.For method in .NET 4, which is used for parallelizing a for loop. In a typical for loop, only one iteration of that loop can execute at any one time, which means that only one exception can occur at a time. With a parallel "loop," however, multiple iterations can execute in parallel, and multiple iterations can throw exceptions concurrently. A single thread calls the Parallel.For method, which can logically throw multiple exceptions, and thus we need a mechanism through which those multiple exceptions can be propagated onto a single thread of execution. Parallel.For handles this by gathering the exceptions thrown and propagating them wrapped in an AggregateException. The rest of the methods on Parallel (Parallel.ForEach and Parallel.Invoke) handle things similarly, as does Parallel LINQ (PLINQ), also part of .NET 4. In a LINQ-to-Objects query, only one user delegate is invoked at a time, but with PLINQ, multiple user delegates can be invoked in parallel, those delegates might throw exceptions, and PLINQ deals with that by gathering them into an AggregateException and propagating that aggregate.
As an example of this kind of parallel execution, consider Figure 3, which shows a method that uses the ThreadPool to invoke multiple user-provided Action delegates in parallel. (A more robust and scalable implementation of this functionality exists in .NET 4 on the Parallel class.) The code uses QueueUserWorkItem to run each Action. If the Action delegate throws an exception, rather than allowing that exception to propagate and go unhandled (which, by default, results in the process being torn down), the code captures the exception and stores it in a list shared by all the work items. After all the asynchronous invocations have completed (successfully or exceptionally), an AggregateException is thrown with the captured exceptions, if any were captured. (Note that this code could be used in OnMyEvent to run all delegates registered with an event in parallel.)
public static void ParallelInvoke(params Action[] actions)
{
    if (actions == null) throw new ArgumentNullException("actions");
    if (actions.Any(a => a == null)) throw new ArgumentException      ("actions");
    if (actions.Length == 0) return;

    using (ManualResetEvent mre = new ManualResetEvent(false)) {
        int remaining = actions.Length;
        var exceptions = new List<Exception>();
        foreach (var action in actions) {
            ThreadPool.QueueUserWorkItem(state => {
                try {
                    ((Action)state)();
                }
                catch (Exception exc) {
                    lock (exceptions) exceptions.Add(exc);
                }
                finally {
                    if (Interlocked.Decrement(ref remaining) == 0) mre.Set();
                }
            }, action);
        }
        mre.WaitOne();
        if (exceptions.Count > 0) throw new AggregateException(exceptions);
    }
}
The new System.Threading.Tasks namespace in .NET 4 also makes liberal use of AggregateExceptions. A Task in .NET 4 is an object that represents an asynchronous operation. Unlike QueueUserWorkItem, which doesn't provide any mechanism to refer back to the queued work, Tasks provides a handle to the asynchronous work, enabling a large number of important operations to be performed, such as waiting for a work item to complete or continuing from it to perform some operation when the work completes. The Parallel methods mentioned earlier are built on top of Tasks, as is PLINQ.
Furthering the discussion of AggregateException, an easy construct to reason about here is the static Task.WaitAll method. You pass to WaitAll all the Task instances you want to wait on, and WaitAll "blocks" until those Task instances have completed. (I've placed quotation marks around "blocks" because the WaitAll method might actually assist in executing the Tasks so as to minimize resource consumption and provide better efficiency than just blocking a thread.) If the Tasks all complete successfully, the code goes on its merry way. However, multiple Tasks might have thrown exceptions, and WaitAll can propagate only one exception to its calling thread, so it wraps the exceptions into a single AggregateException and throws that aggregate.
Tasks use AggregateExceptions in other places as well. One that might not be as obvious is in parent/child relationships between Tasks. By default, Tasks created during the execution of a Task are parented to that Task, providing a form of structured parallelism. For example, Task A creates Task B and Task C, and in doing so Task A is considered the parent of both Task B and Task C. These relationships come into play primarily in regard to lifetimes. A Task isn't considered completed until all its children have completed, so if you used Wait on Task A, that instance of Wait wouldn't return until both B and C had also completed. These parent/child relationships not only affect execution in that regard, but they're also visible through new debugger tool windows in Visual Studio 2010, greatly simplifying the debugging of certain types of workloads.
Consider code like the following:
var a = Task.Factory.StartNew(() => { 
    var b = Task.Factory.StartNew(() => { 
        throw new Exception("uh"); 
    }); 
    var c = Task.Factory.StartNew(() => { 
        throw new Exception("oh"); 
    }); 
});
...
a.Wait();
Here, Task A has two children, which it implicitly waits for before it is considered complete, and both of those children throw unhandled exceptions. To account for this, Task A wraps its children's exceptions into an AggregateException, and it's that aggregate that's returned from A's Exception property and thrown out of a call to Wait on A.
As I've demonstrated, AggregateException can be a very useful tool. For usability and consistency reasons, however, it can also lead to designs that might at first be counterintuitive. To clarify what I mean, consider the following function:
public void DoStuff() 
{
    var inputNum = Int32.Parse(Console.ReadLine()); 
    Parallel.For(0, 4, i=> { 
        if (i < inputNum) throw new MySpecialException(i.ToString()); 
    });
}
Here, depending on user input, the code contained in the parallel loop might throw 0, 1, or more exceptions. Now consider the code you'd have to write to handle those exceptions. If Parallel.For wrapped exceptions in an AggregateException only when multiple exceptions were thrown, you, as the consumer of DoStuff, would need to write two separate catch handlers: one for the case in which only one MySpecialException occurred, and one for the case in which an AggregateException occurred. The code for handling the AggregateException would likely search the AggregateException's InnerExceptions for a MySpecialException and then run the same handling code for that individual exception that you would have in the catch block dedicated to MySpecialException. As you start dealing with more exceptions, this duplication problem grows. To address this problem as well as to provide consistency, methods in .NET 4 like Parallel.For that need to deal with the potential for multiple exceptions always wrap, even if only one exception occurs. That way, you need to write only one catch block for AggregateException. The exception to this rule is that exceptions that may never occur in a concurrent scope will not be wrapped. So, for example, exceptions that might result from Parallel.For due to it validating its arguments and finding one of them to be null will not be wrapped. That argument validation occurs before Parallel.For spins off any asynchronous work, and thus it's impossible that multiple exceptions could occur.
Of course, having exceptions wrapped in an AggregateException can also lead to some difficulties in that you now have two models to deal with: unwrapped and wrapped exceptions. To ease the transition between the two, AggregateException provides several helper methods to make working with these models easier.
The first helper method is Flatten. As I mentioned, AggregateException is itself an Exception, so it can be thrown. This means, however, that AggregateException instances can wrap other AggregateException instances, and, in fact, this is a likely occurrence, especially when dealing with recursive functions that might throw aggregates. By default, AggregateExceptions retains this hierarchical structure, which can be helpful when debugging because the hierarchical structure of the contained aggregates will likely correspond to the structure of the code that threw those exceptions. However, this can also make aggregates more difficult to work with in some cases. To account for that, the Flatten method removes the layers of contained aggregates by creating a new AggregateException that contains the non-AggregateExceptions from the whole hierarchy. As an example, let's say I had the following structure of exception instances:
  • AggregateException
  • InvalidOperationException
  • ArgumentOutOfRangeException
  • AggregateException
  • IOException
  • DivideByZeroException
  • AggregateException
  • FormatException
  • AggregateException
  • TimeZoneException
If I call Flatten on the outer AggregateException instance, I get a new AggregateException with the following structure:
  • AggregateException
  • InvalidOperationException
  • ArgumentOutOfRangeException
  • IOException
  • DivideByZeroException
  • FormatException
  • TimeZoneException
This makes it much easier for me to loop through and examine the InnerExceptions of the aggregate, without having to worry about recursively traversing contained aggregates.
The second helper method, Handle, makes such traversal easier. Handle has the following signature:
public void Handle(Func<Exception,bool> predicate);
Here's an approximation of its implementation:
public void Handle(Func<Exception,bool> predicate) 
{ 
    if (predicate == null) throw new ArgumentNullException("predicate"); 
    List<Exception> remaining = null; 
    foreach(var exception in InnerExceptions) { 
        if (!predicate(exception)) {
            if (remaining == null) remaining = new List<Exception>();
            remaining.Add(exception); 
        }
    } 
    if (remaining != null) throw new AggregateException(remaining); 
}
Handle iterates through the InnerExceptions in the AggregateException and evaluates a predicate function for each. If the predicate function returns true for a given exception instance, that exception is considered handled. If, however, the predicate returns false, that exception is thrown out of Handle again as part of a new AggregateException containing all the exceptions that failed to match the predicate. This approach can be used to quickly filter out exceptions you don't care about; for example:
try { 
    MyOperation();
} 
catch(AggregateException ae) { 
    ae.Handle(e => e is FormatException); 
}
That call to Handle filters out any FormatExceptions from the AggregateException that is caught. If there are exceptions besides FormatExceptions, only those exceptions are thrown again as part of the new AggregateException, and if there aren't any non-FormatException exceptions, Handle returns successfully with nothing being thrown again. In some cases, it might also be useful to first flatten the aggregates, as you see here:
ae.Flatten().Handle(e => e is FormatException);
Of course, at its core an AggregateException is really just a container for other exceptions, and you can write your own helper methods to work with those contained exceptions in a manner that fits your application's needs. For example, maybe you care more about just throwing a single exception than retaining all the exceptions. You could write an extension method like the following:
public static void PropagateOne(this AggregateException aggregate)
{
    if (aggregate == null) throw new ArgumentNullException("aggregate");
    if (aggregate.InnerException != null)
        throw aggregate.InnerException; // just throw one
}
which you could then use as follows:
catch(AggregateException ae) { ae.PropagateOne(); }
Or maybe you want to filter to show only those exceptions that match a certain criteria and then aggregate information about those exceptions. For example, you might have an AggregateException containing a whole bunch of ArgumentExceptions, and you want to summarize which parameters caused the problems:
AggregateException aggregate = ...;
string [] problemParameters = 
    (from exc in aggregate.InnerExceptions
     let argExc = exc as ArgumentException
     where argExc != null && argExc.ParamName != null
     select argExc.ParamName).ToArray();
All in all, the new System.AggregateException is a simple but powerful tool, especially for applications that can't afford to let any exception go unnoticed. For debugging purposes, AggregateException's ToString implementation outputs a string rendering all the contained exceptions. And as you can see back in Figure 1, there's even a DebuggerDisplayAttribute on AggregateException to help you quickly identify how many exceptions an AggregateException contains.

Send your questions and comments for Stephen to netqa@microsoft.com.

Stephen Toub is a Senior Program Manager Lead on the Parallel Computing Platform team at Microsoft. He is also a Contributing Editor for MSDN Magazine.

.NET Themen
Aggregieren von Ausnahmen
Stephen Toub
Ausnahmen in .NET sind die grundlegenden Mechanismen, mit deren Hilfe Fehler und andere Ausnahmebedingungen kommuniziert werden. Ausnahmen werden verwendet, nicht nur um Informationen über derartige Probleme zu speichern, sondern auch die Daten in Objektinstanz Formular über eine Aufrufliste weitergegeben. Basierend auf der Windows strukturierte Ausnahmebehandlung (SEH) Modell, kann nur eine .NET Ausnahme "in Flight" sein.zu jedem bestimmten Zeitpunkt auf alle bestimmten Thread eine Einschränkung, die in der Regel nary einen Gedanken in EntwicklerRecht. Nachdem alle ergibt einen Vorgang in der Regel nur eine Ausnahme, und daher im sequenziellen Code wir schreiben Großteil der Zeit müssen wir nur eine Ausnahme zu einem Zeitpunkt besorgt werden. Es gibt jedoch verschiedene Szenarios, in denen mehrere Ausnahmen von einem Arbeitsgang führen. Dies umfasst (aber ist nicht beschränkt auf) Szenarien im Zusammenhang mit Parallelität und Parallelität.
Betrachten Sie das Auslösen eines Ereignisses in .NET:
public event EventHandler MyEvent;

protected void OnMyEvent() {
    EventHandler handler = MyEvent; 
    if (handler != null) handler(this, EventArgs.Empty); 
}
Können Sie mehrere Stellvertretungen MyEvent eingetragen, und wenn der Handler-Delegat im vorherigen Codeausschnitt aufgerufen wird, der Vorgang ist gleichbedeutend mit dem folgenden Code:
foreach(var d in handler.GetInvocationList()) {
    ((EventHandler)d)(this, EventArgs.Empty); 
}
Jeder Delegat, der macht, bis der Handler Multicastdelegat aufgerufen nacheinander. Wenn aus dem Aufruf eine Ausnahme ausgelöst wird, hört die Foreach-Schleife Verarbeitung, was bedeutet, dass einige Delegaten nicht bei der Ausnahme ausgeführt werden können. Betrachten Sie z. B. den folgenden Code:
MyEvent += (s, e) => Console.WriteLine("1");
MyEvent += (s, e) => Console.WriteLine("2");
MyEvent += (s, e) => { throw new Exception("uh oh"); };
MyEvent += (s, e) => Console.WriteLine("3");
MyEvent += (s, e) => Console.WriteLine("4");
Wenn MyEvent jetzt aufgerufen wird "1"und "2"Ausgabe in der Konsole sind, wird eine Ausnahme ausgelöst und die Delegaten würde haben output "3"und "4"wird nicht aufgerufen werden.
Um sicherzustellen, dass alle Delegaten auch angesichts der Ausnahme aufgerufen werden, können wir unsere OnMyEvent-Methode wie folgt umschreiben:
protected void OnMyEvent() {
    EventHandler handler = MyEvent; 
    if (handler != null) { 
        foreach (var d in handler.GetInvocationList()) { 
            try { 
                ((EventHandler)d)(this, EventArgs.Empty); 
            } 
            catch{}
        } 
    } 
}
Jetzt sind wir abfangen aller Ausnahmen, die von einem registrierten Handler escape ermöglicht Delegaten, die geliefert, nachdem eine Ausnahme weiterhin aufgerufen werden. Wenn Sie das frühere Beispiel erneut ausführen, sehen die "1", "2", "3" und "4" Siesind Sie Ausgabe, obwohl von eines Delegaten eine Ausnahme ausgelöst wird. Leider ist diese neue Implementierung auch die Ausnahmen eine praktische Übung Essen, die stark auf frowned ist. Diese Ausnahmen konnte ein schwerwiegendes Problem mit der Anwendung ein Problem an, die nicht behandelt wird, da die Ausnahmen ignoriert werden.
Was wir wirklich besteht darin, alle Ausnahmen erfassen, die möglicherweise beheben und dann nach wir haben alle Ereignishandler aufrufen throw erneut alle Ausnahmen, die von einem Handler mit Escapezeichen versehen. Natürlich, wie bereits erwähnt, kann nur eine Ausnahmeinstanz auf einem bestimmten Thread zu einem bestimmten Zeitpunkt ausgelöst werden. Geben Sie AggregateException.
In der .NET Framework 4 4, System.AggregateException ist eine neue Ausnahme Art in Mscorlib. Während eines relativ einfachen Typs ermöglicht es eine Vielzahl von Szenarien durch zentrale und nützliche Ausnahme-Funktionen bereitstellen.
AggregateException ist selbst eine Ausnahme (die von System.Exception abgeleitet werden), die andere Ausnahmen enthält. Die System.Exception-Basisklasse hat bereits das Konzept der umbrechen einer einzelnen Ausnahme-Instanz, genannt die "innere Ausnahme". Die innere Ausnahme, durch die InnerException-Eigenschaft auf Ausnahmen, offen gelegt werden die Ursache der Ausnahme darstellt und häufig von Frameworks, Layer Funktionalität und die Ausnahmen, verwenden um die Informationen zu erhöhen verwendet. Beispielsweise kann eine Komponente, die Eingabedaten aus einem Stream analysiert eine IOException beim Lesen aus dem Stream kommen. Es könnte dann ein CustomParserException erstellen, die umschließt die IOException-Instanz als die InnerException, Bereitstellen auf höherer Ebene Details, welche der Analysevorgang während weiterhin die IOException für niedrigerer Ebene bereitstellen und zugrunde liegenden Details falsche aufgetreten.
AggregateException erweitert einfach die Unterstützung für Textumbruch des inneren Ausnahmen aktivieren – im plural. Bietet Konstruktoren, die akzeptieren Params-Arrays oder Enumerables dieser innere Ausnahmen (zusätzlich zu den standardmäßigen Konstruktor, der eine einzelne innere Ausnahme akzeptiert), und es macht die inneren Ausnahmen durch eine InnerExceptions-Eigenschaft (zusätzlich zu der InnerException-Eigenschaft der Basisklasse) verfügbar. Abbildung 1 für eine Übersicht über öffentliche Oberfläche des AggregateException angezeigt.
[Serializable]
[DebuggerDisplay("Count = {InnerExceptions.Count}")]
public class AggregateException : Exception
{
    public AggregateException();
    public AggregateException(params Exception[] innerExceptions);
    public AggregateException(IEnumerable<Exception> innerExceptions);
    public AggregateException(string message);
    public AggregateException(string message, Exception innerException);
    public AggregateException(string message, 
        params Exception[] innerExceptions);
    public AggregateException(string message, 
        IEnumerable<Exception> innerExceptions);

    public AggregateException Flatten();
    public void Handle(Func<Exception, bool> predicate);

    public ReadOnlyCollection<Exception> InnerExceptions { get; }
} 
Wenn die AggregateException inneren Ausnahmen besitzt, InnerException gibt null zurück, und InnerExceptions gibt eine leere Auflistung zurück. Wenn die AggregateException mit einer einzelnen Ausnahme umschließen bereitgestellt wird, gibt InnerException gibt, die Instanz (wie erwartet) und InnerExceptions eine Auflistung mit nur, dass eine Ausnahme zurück. Und wenn die AggregateException mit mehrere Ausnahmen fließen bereitgestellt wird, InnerExceptions gibt die Werte in der Auflistung und InnerException gibt das erste Element aus der Auflistung zurück.
Nun mit AggregateException, können wir unsere .NET ereignisauslösende Code wie im Abbildung 2 erhöhen, und wir haben unsere Kuchen und Essen, zu können. Delegaten das Ereignis registriert weiterhin ausgeführt, selbst wenn eine löst eine Ausnahme aus, und noch wir nicht verlieren die außergewöhnliche Informationen da Sie alle in einem Aggregat gewrappt und erneut am Ende ausgelöst werden, (nur wenn einer der Delegaten, des Kurses fehlschlägt) sind.
protected void OnMyEvent() { 
    EventHandler handler = MyEvent; 
    if (handler != null) { 
        List<Exception> exceptions = null;
        foreach (var d in handler.GetInvocationList()) 
        { 
            try { 
                ((EventHandler)d)(this, EventArgs.Empty); 
            } 
            catch (Exception exc) { 
                if (exceptions == null) 
                    exceptions = new List<Exception>();
                exceptions.Add(exc);
            } 
        } 
        if (exceptions != null) throw new AggregateException(exceptions); 
    } 
}
Ereignisse geben Sie eine Volltonfarbe Beispiel wo Ausnahme Aggregation für sequenziellen Code nützlich ist. Allerdings ist AggregateException auch gutes Wichtigkeit für die neue Parallelität in .NET 4 erstellt (und in der Tat Obwohl AggregateException für nicht parallelen Code nützlich ist, der Typ erstellt und hinzugefügt wurde .NET Framework vom Parallel Computing Platform-Team bei Microsoft).
Berücksichtigen Sie die neue parallel.for-Methode in .NET 4 parallelisieren dient eine for-Schleife. In eine typische for-Schleife kann nur eine Iteration der Schleife zu einem beliebigen Zeitpunkt ausführen, d. h., dass jeweils nur eine Ausnahme auftreten kann. Mit einen parallelen "Schleife"allerdings mehrere Iterationen können parallel ausgeführt und mehrere Iterationen können gleichzeitig lösen Ausnahmen aus. Ein einzelner Thread Ruft die parallel.for-Methode, die logisch mehrere Ausnahmen auslösen kann und wir benötigen daher einen Mechanismus, über dem diese mehrere Ausnahmen auf einen einzigen Ausführungsthread weitergegeben werden können. Parallel.for verarbeitet diese durch Sammeln von ausgelösten Ausnahmen und weitergegeben werden in einer AggregateException eingeschlossen. Der Rest der die Methoden für parallele (parallel.foreach und parallel.Invoke) behandeln Dinge ebenso, wie parallel LINQ (. PLINQ), ebenfalls Teil von .NET 4 unterstützt. In einer LINQ-to-Objects-Abfrage nur ein Benutzer Delegat zu einem Zeitpunkt aufgerufen wird, aber mit PLINQ, können mehrere Benutzer Delegaten parallel aufgerufen werden, diesen Delegaten können Ausnahmen auslösen und PLINQ befasst sich mit, die durch diese in eine AggregateException sammeln und das Aggregat weitergegeben.
Ein Beispiel für diese Art der parallelen Ausführung sollten Sie Abbildung 3, die eine Methode, die den ThreadPool verwendet zeigt, um mehrere Benutzer bereitgestellte Aktion Delegaten parallel aufrufen. (Eine weitere zuverlässige und skalierbare Implementierung dieser Funktion existiert in .NET 4 der parallel-Klasse.) Der Code verwendet QueueUserWorkItem jede Aktion ausführen. Wenn der Delegat Aktion löst eine Ausnahme, statt diese Ausnahme weiterzugeben und wechseln unbehandelte (die standardmäßig führt der Prozess wird unten unterbrochener), wird der Code die Ausnahme erfasst und speichert ihn in eine Liste aller Arbeitsaufgaben freigegeben. Nachdem die asynchrone Aufrufe (erfolgreich oder außergewöhnlich) abgeschlossen haben, wird eine AggregateException mit den aufgezeichneten Ausnahmen ausgelöst, wenn alle erfasst wurden. (Beachten Sie, dass dieser Code in OnMyEvent verwendet werden konnte, um alle Delegaten registriert ein Ereignis parallel ausführen.)
public static void ParallelInvoke(params Action[] actions)
{
    if (actions == null) throw new ArgumentNullException("actions");
    if (actions.Any(a => a == null)) throw new ArgumentException      ("actions");
    if (actions.Length == 0) return;

    using (ManualResetEvent mre = new ManualResetEvent(false)) {
        int remaining = actions.Length;
        var exceptions = new List<Exception>();
        foreach (var action in actions) {
            ThreadPool.QueueUserWorkItem(state => {
                try {
                    ((Action)state)();
                }
                catch (Exception exc) {
                    lock (exceptions) exceptions.Add(exc);
                }
                finally {
                    if (Interlocked.Decrement(ref remaining) == 0) mre.Set();
                }
            }, action);
        }
        mre.WaitOne();
        if (exceptions.Count > 0) throw new AggregateException(exceptions);
    }
}
Der neue System.Threading.Tasks-Namespace in .NET 4 nutzt auch liberaler AggregateExceptions. Eine Aufgabe in .NET 4 ist ein Objekt, die eine asynchrone Operation darstellt. Im Gegensatz zu QueueUserWorkItem, die keinen Mechanismus zurück auf die Arbeit über eine Warteschlange verweisen nicht stellt Aufgaben ein Handle für die asynchrone Arbeit eine große Anzahl wichtige Vorgänge ausgeführt werden, z. B. eine Arbeitsaufgabe abschließen warten oder von ihm einige Vorgang nach Abschluss die Arbeit fortfahren aktivieren. Die parallelen erwähnten Methoden werden von Aufgaben erstellt, wie PLINQ ist.
Die Diskussion AggregateException furthering, ist ein Konstrukt einfach Grund hier die statische Task.WaitAll-Methode. Sie übergeben an WaitAll, alle Aufgaben Instanzen auf warten soll, und WaitAll "Blöcke"bis diese Instanzen Vorgang abgeschlossen haben. (Ich habe Anführungszeichen "Blöcke" platziertDa die WaitAll-Methode tatsächlich unterstützen möglicherweise bei Ausführen der Aufgaben, um minimieren Ressourcenverbrauch und bieten eine bessere Effizienz als nur einen Thread blockiert.) Wenn alle Vorgänge erfolgreich abgeschlossen, wechselt der Code auf dem Fröhliche Weg. Jedoch mehrere Vorgänge möglicherweise Ausnahmen ausgelöst haben, und WaitAll kann nur eine Ausnahme an den aufrufenden Thread übertragen, sodass Ausnahmen in einer einzelnen AggregateException schließt und das Aggregat löst.
Aufgaben verwenden sowie andere Orte AggregateExceptions. Eine, die möglicherweise nicht so offensichtlich ist im übergeordneten/untergeordneten Beziehungen zwischen Aufgaben. Standardmäßig werden Tasks, die während der Ausführung einer Aufgabe erstellt, Aufgabe, eine Form der strukturierte Parallelität bereitstellen übergeordnet. Z. B. Task A B und Task C erstellt und auf diese Weise gilt Task A das übergeordnete Element sowohl B als auch Vorgang. Diese Beziehungen kommen ins Spiel hauptsächlich im Hinblick auf die Gültigkeitsdauer. Ein Vorgang ist nicht als abgeschlossen, bevor alle untergeordneten abgeschlossen haben, sodass Wenn Sie warten auf Vorgang A verwendet, die Instanz des Wait zurückgeben würde nicht, bis B und C auch abgeschlossen wurde. Diese Parent-Child-Beziehungen beeinflussen nicht nur die Ausführung in die Berücksichtigung, aber Sie sind außerdem über neue Debugger-Toolfenster in Visual Studio 2010 erheblich vereinfachen das Debuggen von bestimmten Arten von Arbeitsauslastungen sichtbar.
Betrachten Sie Code wie folgt:
var a = Task.Factory.StartNew(() => { 
    var b = Task.Factory.StartNew(() => { 
        throw new Exception("uh"); 
    }); 
    var c = Task.Factory.StartNew(() => { 
        throw new Exception("oh"); 
    }); 
});
...
a.Wait();
Hier Task A hat zwei untergeordnete Elemente, die es implizit auf, wartet bevor er abgeschlossen, gilt, und beide die untergeordneten unbehandelte Ausnahmen auslösen. Um dies zu berücksichtigen, umschließt Task A die untergeordneten Ausnahmen in einer AggregateException, und ist es, dass Aggregate, die von A zurückgegeben wird Exception-Eigenschaft und außerhalb eines Aufrufs zu warten auf a ausgelöst
Wie ich gezeigt haben, kann AggregateException ein sehr nützliches Tool sein. Gründen der Konsistenz und Benutzerfreundlichkeit kann jedoch es auch zu Designs führen, die auf den ersten Blick counterintuitive sein könnten. Um zu verdeutlichen, was gemeint, sollten Sie die folgende Funktion:
public void DoStuff() 
{
    var inputNum = Int32.Parse(Console.ReadLine()); 
    Parallel.For(0, 4, i=> { 
        if (i < inputNum) throw new MySpecialException(i.ToString()); 
    });
}
Abhängig von der Benutzereingabe kann in der parallelen Schleife enthaltene Code hier 0, 1 oder mehr Ausnahmen auslösen. Betrachten Sie jetzt den Code, den Sie schreiben, um diese Ausnahmen zu behandeln müssen würden. Wenn parallel.for Ausnahmen in einer AggregateException umbrochen nur, wenn mehrere Ausnahmen ausgelöst wurden, würde, als Consumer von DoStuff, müssen Sie zwei separate Catch-Handler schreiben: eine für den Fall, in dem nur ein MySpecialException aufgetreten ist, und eine für den Fall, in dem ein AggregateException aufgetreten ist. Der Code für die Behandlung der AggregateException würde wahrscheinlich eine MySpecialException Suchen der AggregateException-InnerExceptions und führen Sie den gleichen Behandlung Code für die einzelne Ausnahme, die Sie im Catch-Block zu MySpecialException vorgesehen haben würde. Wenn Sie weitere Ausnahmen Umgang beginnen, wächst Problems Duplizierung. Behebung dieses Problems sowie zum Bereitstellen von Konsistenz umschließen, um Methoden in .NET 4 wie parallel.for, die immer das Potenzial für mehrere Ausnahmen behandeln müssen, auch wenn nur eine Ausnahme auftritt. Auf diese Weise müssen Sie nur ein Catch-Block für AggregateException zu schreiben. Die Ausnahme zu dieser Regel ist, dass Ausnahmen, die niemals in einem Bereich gleichzeitig auftreten nicht umbrochen werden. So werden z. B. Ausnahmen die parallel.for aufgrund Argumente überprüft und suchen eine null sein können nicht umbrochen werden. Die Argumentvalidierung erfolgt, bevor parallel.for deaktiviert alle asynchronen Vorgänge rotiert, und es daher unmöglich ist, dass mehrere Ausnahmen auftreten könnten.
Natürlich kann müssen Ausnahmen in einer AggregateException gewrappt auch einige Schwierigkeiten führen in, dass Sie jetzt zwei Modelle für den Umgang mit: entpackten und eingebundenen Ausnahmen. Um den Übergang zwischen den beiden zu erleichtern, stellt AggregateException mehrere Hilfsmethoden arbeiten mit diesen Modellen zu erleichtern.
Die erste Helper-Methode ist Flatten. Wie bereits erwähnt, ist AggregateException selbst eine Ausnahme, die ausgelöst werden können. Dies bedeutet jedoch, dass AggregateException-Instanzen andere AggregateException-Instanzen umbrochen können und, in der Tat dies ein wahrscheinlich auftreten besonders beim Umgang mit rekursiven Funktionen, die Aggregate auslösen könnte. Standardmäßig behält AggregateExceptions dieser hierarchischen Struktur hilfreich sein kann, wenn Debuggen, da die hierarchische Struktur der enthaltenen Aggregate wahrscheinlich die Struktur des Codes zugeordnet werden, die diese Ausnahmen ausgelöst hat. Jedoch kann dies auch Aggregate in einigen Fällen arbeiten mit erschweren. Um für die zu berücksichtigen, entfernt die Flatten-Methode die Ebenen der enthaltenen Aggregate durch Erstellen einer neuen AggregateException, die den nicht-AggregateExceptions aus der gesamten Hierarchie enthält. Als Beispiel angenommen musste Instanzen mit Ausnahme die folgende Struktur:
  • AggregateException
  • InvalidOperationException
  • ArgumentOutOfRangeException
  • AggregateException
  • IOException
  • DivideByZeroException
  • AggregateException
  • FormatException
  • AggregateException
  • TimeZoneException
Wenn Flatten auf der äußeren AggregateException-Instanz aufgerufen werden, wird ein neues AggregateException mit der folgenden Struktur:
  • AggregateException
  • InvalidOperationException
  • ArgumentOutOfRangeException
  • IOException
  • DivideByZeroException
  • FormatException
  • TimeZoneException
Dadurch viel einfacher für mich zu durchlaufen und überprüfen Sie die InnerExceptions des Aggregats, ohne die Aggregate enthalten rekursiv durchlaufen kümmern zu müssen.
Die zweite Hilfsmethode Handle, erleichtert solche Traversal. Handle hat folgende Signatur:
public void Handle(Func<Exception,bool> predicate);
Hier ist eine Näherung der Implementierung:
public void Handle(Func<Exception,bool> predicate) 
{ 
    if (predicate == null) throw new ArgumentNullException("predicate"); 
    List<Exception> remaining = null; 
    foreach(var exception in InnerExceptions) { 
        if (!predicate(exception)) {
            if (remaining == null) remaining = new List<Exception>();
            remaining.Add(exception); 
        }
    } 
    if (remaining != null) throw new AggregateException(remaining); 
}
Handle der InnerExceptions in der AggregateException durchläuft und wertet eine Prädikat-Funktion für jede. Wenn die Prädikatfunktion Ausnahme gilt behandelt gibt True für eine Instanz angegebene Ausnahme zurück. Wenn jedoch das Prädikat false zurückgibt, wird die Ausnahme ausgelöst außerhalb des Handle erneut als Teil einer neuen AggregateException mit den Ausnahmen, die nicht das Prädikat entsprechen. Dieser Ansatz kann schnell Ausnahmen filtern Sie kümmern nicht; verwendet werdenBeispiel:
try { 
    MyOperation();
} 
catch(AggregateException ae) { 
    ae.Handle(e => e is FormatException); 
}
Rufen, um Handles Filter aus alle FormatExceptions aus AggregateException, die abgefangen wird. Wenn von einigen Ausnahmen abgesehen FormatExceptions nur Ausnahmen werden ausgelöst erneut als Teil der neuen AggregateException, und wenn Ausnahmen nicht FormatException vorhanden sind, Handle zurückgegeben erfolgreich mit nichts erneut ausgelöst. In einigen Fällen kann dies auch hilfreich, zunächst die Aggregate zusammenfassen haben wie hier gezeigt:
ae.Flatten().Handle(e => e is FormatException);
Natürlich im Kern ein AggregateException ist lediglich ein Container für andere Ausnahmen, und Sie können Ihre eigenen Hilfsmethoden zum enthaltenen Ausnahmen in einer Weise arbeiten, die Anforderungen der Anwendung entspricht schreiben. Beispielsweise sorgt vielleicht Sie mehr über nur eine einzelne Ausnahme als behalten alle Ausnahmen auslösen. Sie könnten eine Erweiterungsmethode wie folgt schreiben:
public static void PropagateOne(this AggregateException aggregate)
{
    if (aggregate == null) throw new ArgumentNullException("aggregate");
    if (aggregate.InnerException != null)
        throw aggregate.InnerException; // just throw one
}
die Sie dann wie folgt verwenden können:
catch(AggregateException ae) { ae.PropagateOne(); }
Oder vielleicht Sie nur Ausnahmen angezeigt, die mit einem bestimmten Kriterium übereinstimmen und aggregieren Informationen zu diesen Ausnahmen filtern möchten. Beispielsweise möglicherweise haben Sie eine AggregateException enthält eine ganze Reihe von ArgumentExceptions und welche Parameter die Probleme verursacht zusammengefasst werden sollen:
AggregateException aggregate = ...;
string [] problemParameters = 
    (from exc in aggregate.InnerExceptions
     let argExc = exc as ArgumentException
     where argExc != null && argExc.ParamName != null
     select argExc.ParamName).ToArray();
Alles in allem ist neue System.AggregateException eine einfache, aber leistungsfähiges Tool, insbesondere für Anwendungen, die damit jede Ausnahme unbemerkt kann nicht. Für Debugzwecke, gibt der AggregateException ToString Implementierung eine Zeichenfolge Rendern aller enthaltenen Ausnahmen aus. Und wie Sie wieder in Abbildung 1 sehen können, es sogar ein DebuggerDisplayAttribute-AggregateException Ihnen, wie viele Ausnahmen schnell erkennen ein AggregateException enthält.

Senden Sie Ihre Fragen und Kommentare für Stephen in netqa@microsoft.com.

Stephen Toub ist leitender Programmmanager im Parallel Computing Platform-Team bei Microsoft. Er ist außerdem redaktionelle Beiträge für MSDN Magazin.

Page view tracker