Wie kann ich eine wartende Aufgabe abbrechen?

163

Ich spiele mit diesen Windows 8 WinRT-Aufgaben und versuche, eine Aufgabe mit der folgenden Methode abzubrechen. Sie funktioniert bis zu einem gewissen Punkt. Die CancelNotification-Methode wird aufgerufen, was den Eindruck erweckt, dass die Aufgabe abgebrochen wurde. Im Hintergrund wird die Aufgabe jedoch weiter ausgeführt. Nach Abschluss wird der Status der Aufgabe immer abgeschlossen und nie abgebrochen. Gibt es eine Möglichkeit, die Aufgabe vollständig anzuhalten, wenn sie abgebrochen wird?

private async void TryTask()
{
    CancellationTokenSource source = new CancellationTokenSource();
    source.Token.Register(CancelNotification);
    source.CancelAfter(TimeSpan.FromSeconds(1));
    var task = Task<int>.Factory.StartNew(() => slowFunc(1, 2), source.Token);

    await task;            

    if (task.IsCompleted)
    {
        MessageDialog md = new MessageDialog(task.Result.ToString());
        await md.ShowAsync();
    }
    else
    {
        MessageDialog md = new MessageDialog("Uncompleted");
        await md.ShowAsync();
    }
}

private int slowFunc(int a, int b)
{
    string someString = string.Empty;
    for (int i = 0; i < 200000; i++)
    {
        someString += "a";
    }

    return a + b;
}

private void CancelNotification()
{
}
Carlo
quelle
Ich habe gerade diesen Artikel gefunden, der mir geholfen hat, die verschiedenen Möglichkeiten zum Abbrechen zu verstehen.
Uwe Keim

Antworten:

238

Informieren Sie sich über Cancellation (das in .NET 4.0 eingeführt wurde und seitdem weitgehend unverändert ist) und das aufgabenbasierte asynchrone Muster , das Richtlinien zur Verwendung CancellationTokenmit asyncMethoden enthält.

Zusammenfassend übergeben Sie CancellationTokenjeder Methode, die die Stornierung unterstützt, ein a, und diese Methode muss es regelmäßig überprüfen.

private async Task TryTask()
{
  CancellationTokenSource source = new CancellationTokenSource();
  source.CancelAfter(TimeSpan.FromSeconds(1));
  Task<int> task = Task.Run(() => slowFunc(1, 2, source.Token), source.Token);

  // (A canceled task will raise an exception when awaited).
  await task;
}

private int slowFunc(int a, int b, CancellationToken cancellationToken)
{
  string someString = string.Empty;
  for (int i = 0; i < 200000; i++)
  {
    someString += "a";
    if (i % 1000 == 0)
      cancellationToken.ThrowIfCancellationRequested();
  }

  return a + b;
}
Stephen Cleary
quelle
2
Wow tolle Infos! Das hat perfekt funktioniert, jetzt muss ich herausfinden, wie die Ausnahme in der asynchronen Methode behandelt wird. Danke, Mann! Ich werde das Zeug lesen, das du vorgeschlagen hast.
Carlo
8
Nein. Die meisten lang laufenden synchronen Methoden haben eine Möglichkeit, sie abzubrechen - manchmal durch Schließen einer zugrunde liegenden Ressource oder Aufrufen einer anderen Methode. CancellationTokenverfügt über alle erforderlichen Hooks, um mit benutzerdefinierten Stornierungssystemen zusammenzuarbeiten, aber nichts kann eine nicht stornierbare Methode abbrechen.
Stephen Cleary
1
Ah ich sehe. Der beste Weg, um die ProcessCancelledException abzufangen, besteht darin, das 'Warten' in einen Versuch / Fang einzuschließen. Manchmal bekomme ich die AggregatedException und kann damit nicht umgehen.
Carlo
3
Richtig. Ich empfehle, dass Sie niemals Waitoder Resultin asyncMethoden verwenden; Sie sollten awaitstattdessen immer verwenden , wodurch die Ausnahme korrekt ausgepackt wird.
Stephen Cleary
11
Nur neugierig, gibt es einen Grund, warum keines der Beispiele CancellationToken.IsCancellationRequestedAusnahmen verwendet und stattdessen vorschlägt?
James M
41

Oder um Änderungen zu vermeiden slowFunc(sagen wir, Sie haben beispielsweise keinen Zugriff auf den Quellcode):

var source = new CancellationTokenSource(); //original code
source.Token.Register(CancelNotification); //original code
source.CancelAfter(TimeSpan.FromSeconds(1)); //original code
var completionSource = new TaskCompletionSource<object>(); //New code
source.Token.Register(() => completionSource.TrySetCanceled()); //New code
var task = Task<int>.Factory.StartNew(() => slowFunc(1, 2), source.Token); //original code

//original code: await task;  
await Task.WhenAny(task, completionSource.Task); //New code

Sie können auch nette Erweiterungsmethoden von https://github.com/StephenCleary/AsyncEx verwenden und es sieht so einfach aus wie:

await Task.WhenAny(task, source.Token.AsTask());
Sonatique
quelle
1
Es sieht sehr knifflig aus ... als ganze asynchrone Implementierung. Ich glaube nicht, dass solche Konstruktionen den Quellcode lesbarer machen.
Maxim
1
Vielen Dank, ein Hinweis - das Registrieren von Token sollte später entsorgt werden, das zweite - Verwenden ConfigureAwaitSie es, da Sie sonst in UI-Apps verletzt werden können.
Astrowalker
@astrowalker: Ja, in der Tat sollte die Registrierung eines Tokens besser nicht registriert (entsorgt) werden. Dies kann innerhalb des Delegaten erfolgen, der an Register () übergeben wird, indem dispose für das von Register () zurückgegebene Objekt aufgerufen wird. Da das "Quell"
-Token
1
Eigentlich braucht es nur, um es zu verschachteln using.
Astrowalker
@astrowalker ;-) ja du hast eigentlich recht. In diesem Fall ist dies die viel einfachere Lösung! Wenn Sie Task.WhenAny jedoch direkt (ohne Wartezeit) zurückgeben möchten, benötigen Sie etwas anderes. Ich sage das, weil ich einmal auf ein Refactoring-Problem wie dieses gestoßen bin: Bevor ich ... warten musste. Ich habe dann das Warten (und das Asynchronisieren der Funktion) entfernt, da es das einzige war, ohne zu bemerken, dass ich den Code vollständig gebrochen habe. Der resultierende Fehler war schwer zu finden. Ich zögere daher, using () zusammen mit async / await zu verwenden. Ich habe das Gefühl, dass das Dispose-Muster sowieso nicht gut zu asynchronen Dingen
passt
15

Ein Fall, der nicht behandelt wurde, ist die Behandlung der Stornierung innerhalb einer asynchronen Methode. Nehmen wir zum Beispiel einen einfachen Fall, in dem Sie einige Daten in einen Dienst hochladen müssen, um sie zu berechnen und dann einige Ergebnisse zurückzugeben.

public async Task<Results> ProcessDataAsync(MyData data)
{
    var client = await GetClientAsync();
    await client.UploadDataAsync(data);
    await client.CalculateAsync();
    return await client.GetResultsAsync();
}

Wenn Sie die Stornierung unterstützen möchten, ist es am einfachsten, ein Token zu übergeben und zu überprüfen, ob es zwischen den einzelnen asynchronen Methodenaufrufen abgebrochen wurde (oder ContinueWith verwendet). Wenn es sich um sehr lange laufende Anrufe handelt, können Sie eine Weile warten, bis sie abgebrochen werden. Ich habe eine kleine Hilfsmethode erstellt, die stattdessen fehlschlägt, sobald sie abgebrochen wird.

public static class TaskExtensions
{
    public static async Task<T> WaitOrCancel<T>(this Task<T> task, CancellationToken token)
    {
        token.ThrowIfCancellationRequested();
        await Task.WhenAny(task, token.WhenCanceled());
        token.ThrowIfCancellationRequested();

        return await task;
    }

    public static Task WhenCanceled(this CancellationToken cancellationToken)
    {
        var tcs = new TaskCompletionSource<bool>();
        cancellationToken.Register(s => ((TaskCompletionSource<bool>)s).SetResult(true), tcs);
        return tcs.Task;
    }
}

Um es zu verwenden, fügen Sie es einfach .WaitOrCancel(token)zu jedem asynchronen Aufruf hinzu:

public async Task<Results> ProcessDataAsync(MyData data, CancellationToken token)
{
    Client client;
    try
    {
        client = await GetClientAsync().WaitOrCancel(token);
        await client.UploadDataAsync(data).WaitOrCancel(token);
        await client.CalculateAsync().WaitOrCancel(token);
        return await client.GetResultsAsync().WaitOrCancel(token);
    }
    catch (OperationCanceledException)
    {
        if (client != null)
            await client.CancelAsync();
        throw;
    }
}

Beachten Sie, dass die Aufgabe, auf die Sie gewartet haben, dadurch nicht gestoppt wird und weiterhin ausgeführt wird. Sie müssen einen anderen Mechanismus verwenden, um ihn zu stoppen, z. B. den CancelAsyncAnruf im Beispiel, oder besser noch denselben CancellationTokenan den übergeben, Taskdamit er die Stornierung eventuell verarbeiten kann. Der Versuch, den Thread abzubrechen, wird nicht empfohlen .

kjbartel
quelle
1
Beachten Sie, dass das Warten auf die Aufgabe zwar abgebrochen wird, die eigentliche Aufgabe jedoch nicht abgebrochen wird (z. B. UploadDataAsyncim Hintergrund fortgesetzt werden kann, nach Abschluss jedoch nicht mehr aufgerufen wird, CalculateAsyncda dieser Teil bereits nicht mehr wartet). Dies kann für Sie problematisch sein oder auch nicht, insbesondere wenn Sie den Vorgang wiederholen möchten. CancellationTokenWenn möglich, ist es die bevorzugte Option, den gesamten Weg nach unten zu gehen.
Miral
1
@Miral das stimmt, aber es gibt viele asynchrone Methoden, die keine Stornierungs-Token akzeptieren. Nehmen wir zum Beispiel WCF-Dienste, die beim Generieren eines Clients mit Async-Methoden keine Stornierungs-Token enthalten. Wie das Beispiel zeigt und Stephen Cleary ebenfalls feststellte, wird davon ausgegangen, dass lange laufende synchrone Aufgaben eine Möglichkeit haben, sie abzubrechen.
Kjbartel
1
Deshalb habe ich "wenn möglich" gesagt. Meistens wollte ich nur, dass diese Einschränkung erwähnt wird, damit die Leute, die diese Antwort später finden, nicht den falschen Eindruck bekommen.
Miral
@ Miral Danke. Ich habe aktualisiert, um diese Einschränkung widerzuspiegeln.
Kjbartel
Leider funktioniert dies nicht mit Methoden wie 'NetworkStream.WriteAsync'.
Zeokat
6

Ich möchte nur die bereits akzeptierte Antwort ergänzen. Ich blieb dabei, aber ich ging einen anderen Weg, um das gesamte Ereignis zu bewältigen. Anstatt zu warten, füge ich der Aufgabe einen fertigen Handler hinzu.

Comments.AsAsyncAction().Completed += new AsyncActionCompletedHandler(CommentLoadComplete);

Wo der Event-Handler so aussieht

private void CommentLoadComplete(IAsyncAction sender, AsyncStatus status )
{
    if (status == AsyncStatus.Canceled)
    {
        return;
    }
    CommentsItemsControl.ItemsSource = Comments.Result;
    CommentScrollViewer.ScrollToVerticalOffset(0);
    CommentScrollViewer.Visibility = Visibility.Visible;
    CommentProgressRing.Visibility = Visibility.Collapsed;
}

Mit dieser Route ist die gesamte Bearbeitung bereits für Sie erledigt. Wenn die Aufgabe abgebrochen wird, wird nur die Ereignisbehandlungsroutine ausgelöst, und Sie können sehen, ob sie dort abgebrochen wurde.

Smeegs
quelle