Ein delegate Task repräsentiert eine CPU-bound Aufgabe die durch den Task Scheduler einem Thread zugewiesen wird. In Teil 8 wurde gezeigt wie man einen delegate Task mit der TPL erstellt und in Teil 9 wurde das korrekte Abbrechen einer Task behandelt. Dieser Teil zeigt, wie man Task Exceptions richtig verarbeitet und wie ein Task richtig wartet und Fortschritt melden kann.
Auftretende Exceptions werden in der Exception Eigenschaft vom Task Objekt gespeichert. Weil Tasks verschachtelt sein können, wird dazu der Typ AggregateException verwendet.
Es gibt im AggregateException Objekt zwei Eigenschaften:
Bei der TPL muss man immer die AggregateException abfangen und die InnerExceptions Sammlung auswerten. Diese sind je nach Hierarchie ebenfalls verschachtelt. Um die Hierarchie flach zu machen, gibt es den Aufruf ‚Flatten‘.
List<Exception> exceptions = aggregateException.Flatten().InnerExceptions.Where(e => !(e is OperationCanceledException)).ToList();
Wie beim Result kommt man erst an die Exceptions ran, wenn der Task fertig ist. Siehe den Abschnitt „Rückgabewerte“ vom Teil 8.
Wenn der Benutzer vergisst eine Task Ausnahme abzufangen gehen diese Ausnahmen normalerweise verloren. Es gibt aber noch eine letzte Chance, diese Ausnahmen abzufangen und zu verarbeiten mit dem UnobserverdTaskException Event. Das Prinzip ist ähnlich wie beim UnhandledException Event der AppDomain oder Dispatcher Klasse. Der Event wird erst ausgelöst, wenn der Garbage Collecter das Task-Objekt aufräumt und feststellt, dass die Exception nicht verarbeitet wurde.
public MainWindow() { _cancellationToken = _cancellationTokenSource.Token; InitializeComponent(); AppDomain currentDomain = AppDomain.CurrentDomain; currentDomain.UnhandledException += AppDomain_UnhandledException; Application.Current.DispatcherUnhandledException += Application_DispatcherUnhandledException; TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException; } private void Application_DispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs dispatcherUnhandledExceptionEventArgs) { Debug.WriteLine($"Unhandled exception caught on UI Thread {dispatcherUnhandledExceptionEventArgs.Exception}"); } void AppDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e) { Debug.WriteLine($"Unhandled exception caught of any thread in AppDomain {e.ExceptionObject}"); } void TaskScheduler_UnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e) { Debug.WriteLine($"Unhandled exception found in disposed Task oject {e.Exception}"); }
Als Alternative kann man in der Konfigurationssektion Folgendes einfügen:
<configuration> <runtime> <ThrowUnobservedTaskExceptions enabled=”true”/> </runtime> </configuration>
Mit diesem Setting wird das Programm beendet wenn eine Unobserved Exception durch den Garbage Collector entdeckt wird.
Statt Thread.Sleep() soll man Task.Delay(milliseconds, CancellationToken).Wait(CancellationToken) verwenden. Dieser bricht das Warten ab wenn das CancellationToken aktiviert wurde und wirft eine OperationCanceledException.
Statt Thread.Sleep die Methode Task.Delay().Wait() mit 2x CancellationToken verwenden.
Beispiel: Task.Delay(1000, _cancellationToken).Wait(_cancellationToken);
Als Alternative kann man WaitHandle.WaitOne() vom Token verwenden. Dieser bricht das Warten ab wenn das CancellationToken aktiviert wurde, wirft aber keine Exception. Mann muss dann immer noch selber die OperationCanceledException werfen.
Wenn eine asynchrone Methode Vorschritt melden möchte, kann beim Aufruf eine Implementation von IProgress<T> übergeben werden. Die Schnittstelle hat nur eine Methode: Progress<T>() welcher auf dem Kontext ausgeführt wird worin das Progress Objekt erstellt wurde. Man muss den Aufruf also nicht remarshallen zum UI Thread.
Als alternative kann man auch den Eventhandler ProgressChanged benutzen um den Vorschritt zu melden.
private async void BtnStartTaskRunProgress_Click(object sender, RoutedEventArgs e) { _progress.Foreground = new SolidColorBrush(Color.FromRgb(0, 0, 255)); // The Progress<T> constructor captures the UI context so the lambda will be run on the UI thread. var progress = new Progress<int>(percent => { _progress.Value = percent; }); try { // DoProcessing is run on the thread pool. await Task.Run(() => DoProcessing(_cancellationToken, progress), _cancellationToken); } catch (OperationCanceledException) { _progress.Foreground = new SolidColorBrush(Color.FromRgb(255, 0, 0)); return; } _progress.Value = 100; _progress.Foreground = new SolidColorBrush(Color.FromRgb(0,255,0)); } public void DoProcessing(CancellationToken cancellationToken, IProgress<int> progress) { for (int i = 0; i < 100; ++i) { cancellationToken.ThrowIfCancellationRequested(); // Waiting with cancellation support Task.Delay(100, cancellationToken).Wait(cancellationToken); progress?.Report(i); } }
Im nächsten Blog werden promise Tasks für IO-bound Operationen erklärt zusammen mit den Statements async\await.
[…] ← Vorige Post […]