Jak anulować oczekujące zadanie?

164

Gram z tymi zadaniami Windows 8 WinRT i próbuję anulować zadanie przy użyciu poniższej metody i do pewnego momentu działa. Metoda CancelNotification DOES jest wywoływana, co powoduje, że myślisz, że zadanie zostało anulowane, ale w tle zadanie jest nadal uruchomione, a po jego zakończeniu stan zadania jest zawsze ukończony i nigdy nie jest anulowany. Czy istnieje sposób, aby całkowicie zatrzymać zadanie, gdy zostanie anulowane?

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
źródło
Właśnie znalazłem ten artykuł, który pomógł mi zrozumieć różne sposoby anulowania.
Uwe Keim,

Odpowiedzi:

239

Przeczytaj informacje na temat anulowania (które zostało wprowadzone w .NET 4.0 i od tego czasu w dużej mierze nie uległo zmianie) oraz wzorca asynchronicznego opartego na zadaniach , który zawiera wytyczne dotyczące używania CancellationTokenz asyncmetodami.

Podsumowując, należy przekazać CancellationTokendo każdej metody, która obsługuje anulowanie, a ta metoda musi ją okresowo sprawdzać.

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
źródło
2
Wow świetne informacje! To zadziałało idealnie, teraz muszę dowiedzieć się, jak obsłużyć wyjątek w metodzie asynchronicznej. Dzięki! Przeczytam rzeczy, które zasugerowałeś.
Carlo,
8
Nie. Większość długotrwałych metod synchronicznych ma jakiś sposób na ich anulowanie - czasami przez zamknięcie podstawowego zasobu lub wywołanie innej metody. CancellationTokenma wszystkie zaczepy niezbędne do współdziałania z niestandardowymi systemami anulowania, ale nic nie może anulować metody, której nie można anulować.
Stephen Cleary,
1
O, rozumiem. Zatem najlepszym sposobem na przechwycenie wyjątku ProcessCancelledException jest zawijanie „await” w try / catch? Czasami otrzymuję wyjątek AggregatedException i nie mogę tego obsłużyć.
Carlo,
3
Dobrze. I polecam , że nigdy nie używasz Waitlub Resultw asyncmetodach; powinieneś zawsze używać awaitzamiast tego, co poprawnie rozpakuje wyjątek.
Stephen Cleary,
11
Ciekawe, czy istnieje powód, dla którego żaden z przykładów nie używa, CancellationToken.IsCancellationRequesteda zamiast tego sugeruje rzucanie wyjątków?
James M
41

Lub, aby uniknąć modyfikacji slowFunc(powiedzmy, że nie masz dostępu do kodu źródłowego na przykład):

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

Możesz także użyć ładnych metod rozszerzeń z https://github.com/StephenCleary/AsyncEx i wygląda to tak prosto, jak:

await Task.WhenAny(task, source.Token.AsTask());
sonatique
źródło
1
Wygląda to na bardzo trudne ... jako całość implementacji async-await. Nie sądzę, żeby takie konstrukcje czynią kod źródłowy bardziej czytelnym.
Maxim
1
Dziękuję, jedna uwaga - token rejestracji należy później wyrzucić, druga sprawa - użyj w ConfigureAwaitprzeciwnym razie możesz zranić się w aplikacjach UI.
astrowalker
@astrowalker: tak rzeczywiście rejestracja tokena powinna zostać wyrejestrowana (usunięta). Można to zrobić wewnątrz delegata, który jest przekazywany do Register (), wywołując metodę dispose na obiekcie, który jest zwracany przez Register (). Ponieważ jednak token „source” jest w tym przypadku tylko lokalny, wszystko i tak zostanie wyczyszczone ...
sonatique
1
Właściwie wystarczy go umieścić using.
astrowalker
@astrowalker ;-) tak, właściwie masz rację. W tym przypadku jest to znacznie prostsze rozwiązanie! Jeśli jednak chcesz zwrócić Task.WhenAny bezpośrednio (bez czekania), potrzebujesz czegoś innego. Mówię to, ponieważ raz napotkałem problem refaktoryzacji, taki jak ten: zanim użyłem ... czekaj. Następnie usunąłem await (i async w funkcji), ponieważ był to jedyny, nie zauważając, że całkowicie złamałem kod. Wynikowy błąd był trudny do znalezienia. Dlatego niechętnie używam using () razem z async / await. Czuję, że wzorzec Dispose i tak nie
pasuje do
15

Jedynym przypadkiem, który nie został omówiony, jest sposób obsługi anulowania w ramach metody asynchronicznej. Weźmy na przykład prosty przypadek, w którym musisz przesłać jakieś dane do usługi, aby coś obliczył, a następnie zwróci niektóre wyniki.

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

Jeśli chcesz obsługiwać anulowanie, najłatwiejszym sposobem byłoby przekazanie tokenu i sprawdzenie, czy został on anulowany między każdym wywołaniem metody asynchronicznej (lub przy użyciu ContinueWith). Jeśli są to bardzo długo trwające połączenia, możesz chwilę poczekać, aby je anulować. Stworzyłem małą metodę pomocniczą, która zamiast tego kończyła się niepowodzeniem po anulowaniu.

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;
    }
}

Aby go użyć, po prostu dodaj .WaitOrCancel(token)do dowolnego wywołania asynchronicznego:

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;
    }
}

Pamiętaj, że nie zatrzyma to zadania, na które czekałeś, i będzie kontynuowane. Będziesz musiał użyć innego mechanizmu, aby go zatrzymać, takiego jak CancelAsyncwywołanie w przykładzie, lub jeszcze lepiej przekazać to samo CancellationTokendo, Taskaby ostatecznie obsłużyć anulowanie. Próba przerwania wątku nie jest zalecana .

kjbartel
źródło
1
Zauważ, że chociaż anuluje to oczekiwanie na zadanie, nie anuluje rzeczywistego zadania (więc np. UploadDataAsyncMoże kontynuować w tle, ale po zakończeniu nie wykona połączenia, CalculateAsyncponieważ ta część już przestała czekać). Może to być problematyczne, ale nie musi, zwłaszcza jeśli chcesz ponowić operację. Jeśli CancellationTokenjest to możliwe, preferowaną opcją jest przejście przez całą drogę w dół.
Miral
1
@Miral to prawda, jednak istnieje wiele metod asynchronicznych, które nie pobierają tokenów anulowania. Weźmy na przykład usługi WCF, które podczas generowania klienta z metodami Async nie będą zawierać tokenów anulowania. Rzeczywiście, jak pokazuje przykład i jak zauważył również Stephen Cleary, zakłada się, że długotrwałe zadania synchroniczne mogą w jakiś sposób je anulować.
kjbartel
1
Dlatego powiedziałem „kiedy to możliwe”. Przede wszystkim chciałem po prostu wspomnieć o tym zastrzeżeniu, aby ludzie, którzy później znajdą tę odpowiedź, nie odnieśli złego wrażenia.
Miral
@Miral Thanks. Zaktualizowałem, aby odzwierciedlić to zastrzeżenie.
kjbartel
Niestety nie działa to z metodami takimi jak „NetworkStream.WriteAsync”.
Zeokat
6

Chcę tylko dodać do już zaakceptowanej odpowiedzi. Utknąłem w tym, ale szedłem inną drogą, jeśli chodzi o obsługę całego zdarzenia. Zamiast uruchamiać czekaj, dodaję ukończoną procedurę obsługi do zadania.

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

Gdzie program obsługi zdarzeń wygląda tak

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;
}

Dzięki tej trasie cała obsługa jest już wykonana za Ciebie, gdy zadanie jest anulowane, po prostu wyzwala procedurę obsługi zdarzenia i możesz zobaczyć, czy zostało anulowane.

Smeegs
źródło