en / de
Expertisen
Methoden
Dienstleistungen
Referenzen
Jobs & Karriere
Firma
Technologie-Trends TechCast WebCast TechBlog News Events Academy

C# Concurrency Teil 9: Delegate Task Cancellation

Ein delegate Task repräsentiert eine CPU-bound Aufgabe die durch den Task Scheduler einem Thread zugewiesen wird. Im letzten Blog wurde gezeigt wie ein delegate Task mit TPL erstellt wird. Dieser Blog beschreibt das kontrollierte Herunterfahren von einem Task: Task Abbruch (Task Cancellation).

Der Cancellation Mechanismus

Damit der Task die Gelegenheit hat sauber herunter zu fahren wird ein Cancellation Token verwendet. Das .NET Framework benutzt dazu zwei Klassen: CancellationTokenSource und das Token selber: das CancellationToken.

Wenn der Haupt-Thread CancellationTokenSource.Cancel() aufruft wird das Flag IsCancellationRequested vom CancellationToken gesetzt. Der Task kann dann eventuelle Aufräumarbeiten machen und am Ende eine OperationCanceledException werfen. Dies ist zwingend, da sonst der Task Status nicht auf Canceled gesetzt wird und eventuelle Task Continuation Verkettungen nicht funktionieren.

Damit der Task auch wirklich den Zustand Canceled bekommt, sollte der Task immer bei Abbruch eine OperationCanceledException werfen.

Der Debugger stoppt leider bei dieser Exception. Deaktiviere “Enable Just My Code” in „Options and Settings\Debug“ um dies zu verhindern.

Das CancellationToken ist ThreadSafe: der Task darf also direkt das Token prüfen während der Haupt-Thread möglicherweise das Token aktiviert. Trotzdem sieht man oft, dass das CancellationToken als Parameter den Task übergeben wird, ein möglicher Grund dafür ist die Definition einer sauberen Schnittstelle (vs. dependency hiding).

Die Hälfte der Überladungen von Task.Run() bzw. Task<>.Run() hat als Parameter das CancellationToken. Das führt zu einer grossen Verwirrung weil mancher Anfänger denkt, dass dadurch der Task automatisch abgebrochen wird.

Der CancellationToken Parameter in Task.Run() bricht den Task nicht ab, sondern verhindert nur, dass der Task gestartet wird wenn das CancellationToken bereits gesetzt ist.

Benutze immer die Überladung Task Run(Action, CancellationToken) bzw. Task<TResult> Run<TResult>(Func<TResult>, CancellationToken).

Obwohl CancellationTokenSource IDisposable implementiert findet man kaum ein Beispiel in Internet wo Dispose auch tatsächlich aufgerufen wird.

Man muss Dispose() vom CancellationTokenSource aufrufen sonst bekommt man memory leaks.

Beispiel

Hier ein Beispiel ohne Parameter und Rückgabewert:

private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
private readonly CancellationToken _cancellationToken;

public MainWindow()
{
    InitializeComponent();
    _cancellationToken = _cancellationTokenSource.Token;
}

private void BtnStartTaskRunCancellation_Click(object sender, RoutedEventArgs e)
{
    Task.Run(() => TaskRunCancellation(), _cancellationToken);
}

private void BtnCancelTaskRunCancellation_Click(object sender, RoutedEventArgs e)
{
    _cancellationTokenSource.Cancel();
}

private void TaskRunCancellation()
{
    try
    {
        while (true)
        {
            // Manual polling
            if (_cancellationToken.IsCancellationRequested)
            {
                // Cleanup stuff here
                _cancellationToken.ThrowIfCancellationRequested();
            }
            // Combined
            _cancellationToken.ThrowIfCancellationRequested();
            // Wait or cancel
            Task.Delay(10000, _cancellationToken).Wait(_cancellationToken);
        }
    }
    finally 
    {
        // Or cleanup stuff here
    }
}

public void Dispose()
{
     _cancellationTokenSource?.Dispose();
}

protected override void OnClosed(EventArgs e)
{
     Dispose();
     base.OnClosed(e);
        }

Man kann mit IsCancellationRequested das Token abfragen, etwas machen und dann eine OperationCanceledException werfen oder direkt ThrowIfCancellationRequested() aufrufen.

Diverses

OperationCanceledException richtig werfen

Achtung: folgender Code setzt den Task in Status Faulted statt Canceled!

// Caution: bad code
if (_measurementToken.IsCancellationRequested)
{
    throw new OperationCanceledException();
}

Entweder das CancellationToken als Argument mitgeben:

if (_measurementToken.IsCancellationRequested)
{
    throw new OperationCanceledException(_measurementToken);
}

Oder so:

if (_measurementToken.IsCancellationRequested)
{
   // Cleanup stuff here
   _measurementToken.ThrowIfCancellationRequested();
}

TaskCanceledException versus OperationCanceledException

OperationCanceledException ist die Basisklasse von TaskCanceledException. Manche Operationen werfen nur OperationCanceledException weil keine Tasks involviert sind (z.B. BlockingCollection.TryTake()).

Um sicher zu sein immer OperationCanceledException abfangen an Stelle von TaskCanceledException.

CancellationToken.None

Wenn alle Überladungen der asynchronen Methoden einen CancellationToken Parameter haben und im Clientcode steht kein CancellationToken zur Verfügung, dann kann beim Aufruf CancellationToken.None mitgeben werden.

Follow up

Im nächsten Blog wird das Exception Handling besprochen und es wird gezeigt wie ein Task richtig warten und Fortschritt melden kann.

← Vorige Post
Nächster Post →
Kommentare

Eine Antwort zu “C# Concurrency Teil 9: Delegate Task Cancellation”

  1. […] 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 […]

Schreiben Sie einen Kommentar

Ihre E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

Newsletter - aktuelle Angebote, exklusive Tipps und spannende Neuigkeiten

 Jetzt anmelden
NACH OBEN
Zur Webcast Übersicht