Come annullare un'attività in attesa?


164

Sto giocando con queste attività di Windows 8 WinRT e sto cercando di annullare un'attività utilizzando il metodo seguente e funziona fino a un certo punto. Viene chiamato il metodo CancelNotification, che ti fa pensare che l'attività sia stata annullata, ma in background l'attività continua a funzionare, quindi dopo che è stata completata, lo stato dell'attività è sempre completato e mai annullato. C'è un modo per interrompere completamente l'attività quando viene annullata?

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()
{
}

Ho appena trovato questo articolo che mi ha aiutato a capire i vari modi per annullare.
Uwe Keim,

Risposte:


239

Leggi su cancellazione (che è stato introdotto in .NET 4.0 ed è in gran parte invariato da allora) e il modello asincrono Task-Based , che fornisce le linee guida su come utilizzare CancellationTokencon asyncmetodi.

Per riassumere, si passa a CancellationTokenin ogni metodo che supporta la cancellazione e tale metodo deve verificarlo periodicamente.

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

2
Wow grandi informazioni! Funzionava perfettamente, ora ho bisogno di capire come gestire l'eccezione nel metodo asincrono. Grazie uomo! Leggerò le cose che hai suggerito.
Carlo,

8
No. La maggior parte dei metodi sincroni di lunga durata hanno un modo per annullarli, a volte chiudendo una risorsa sottostante o chiamando un altro metodo. CancellationTokenha tutti i ganci necessari per interagire con i sistemi di cancellazione personalizzati, ma nulla può annullare un metodo non cancellabile.
Stephen Cleary,

1
Ah capisco Quindi il modo migliore per catturare ProcessCancelledException è racchiudere il "wait" in un try / catch? A volte ottengo AggregatedException e non riesco a gestirlo.
Carlo,

3
Destra. Vi consiglio di non usare mai Waito Resultnei asyncmetodi; dovresti sempre usare awaitinvece, che scartare correttamente l'eccezione.
Stephen Cleary,

11
Solo curioso, c'è un motivo per cui nessuno degli esempi usa CancellationToken.IsCancellationRequestede invece suggerisce di gettare eccezioni?
James M,

41

Oppure, per evitare modifiche slowFunc(supponiamo che tu non abbia accesso al codice sorgente, ad esempio):

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

Puoi anche usare dei bei metodi di estensione da https://github.com/StephenCleary/AsyncEx e avere un aspetto semplice come:

await Task.WhenAny(task, source.Token.AsTask());

1
Sembra molto complicato ... poiché tutta l'implementazione in attesa asincrona. Non penso che tali costruzioni rendano più leggibile il codice sorgente.
Massima

1
Grazie, una nota: la registrazione del token dovrebbe essere successivamente eliminata, seconda cosa: utilizzare ConfigureAwaitaltrimenti si potrebbe essere feriti nelle app UI.
astrowalker

@astrowalker: sì, in effetti è meglio che la registrazione del token non sia registrata (eliminata). Questo può essere fatto all'interno del delegato passato a Register () chiamando dispose sull'oggetto restituito da Register (). Tuttavia, poiché il token "sorgente" è solo locale in questo caso, tutto verrà eliminato comunque ...
Sonatique

1
In realtà tutto ciò che serve è annidarlo using.
astrowalker,

@astrowalker ;-) sì, in realtà hai ragione. In questo caso questa è la soluzione molto più semplice! Tuttavia, se si desidera restituire Task.WhenAny direttamente (senza attendere), è necessario qualcos'altro. Lo dico perché una volta ho riscontrato un problema di refactoring come questo: prima di usare ... wait. Ho quindi rimosso l'attesivo (e l'asincrono sulla funzione) poiché era l'unico, senza notare che stavo completamente rompendo il codice. Il bug risultante era difficile da trovare. Sono quindi riluttante a usare using () insieme a async / waitit. Sento che il modello Dispose non va bene con le cose asincrone comunque ...
sonatique

15

Un caso che non è stato trattato è come gestire la cancellazione all'interno di un metodo asincrono. Prendiamo ad esempio un semplice caso in cui è necessario caricare alcuni dati su un servizio per ottenerlo per calcolare qualcosa e quindi restituire alcuni risultati.

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

Se si desidera supportare la cancellazione, il modo più semplice sarebbe passare un token e verificare se è stato cancellato tra ogni chiamata del metodo asincrono (o utilizzando ContinueWith). Se si tratta di chiamate molto lunghe, è possibile che sia necessario attendere un po 'per annullare. Ho creato un piccolo metodo di supporto per fallire invece non appena cancellato.

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

Quindi per usarlo basta aggiungere .WaitOrCancel(token)a qualsiasi chiamata asincrona:

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

Nota che questo non fermerà l'attività che stavi aspettando e continuerà a funzionare. Dovrai utilizzare un meccanismo diverso per interromperlo, come la CancelAsyncchiamata nell'esempio, o meglio passare lo stesso CancellationTokena quello in Taskmodo che possa gestire la cancellazione alla fine. Non è consigliabile provare a interrompere il thread .


1
Si noti che mentre questo annulla l'attesa per l'attività, non annulla l'attività effettiva (quindi, ad esempio, UploadDataAsyncpuò continuare in background, ma una volta completata non effettuerà la chiamata CalculateAsyncperché quella parte ha già smesso di attendere). Questo può essere o non essere problematico per te, specialmente se desideri riprovare l'operazione. Passare CancellationTokenfino in fondo è l'opzione preferita, quando possibile.
Miral,

1
@Miral è vero, tuttavia ci sono molti metodi asincroni che non accettano i token di cancellazione. Prendiamo ad esempio i servizi WCF, che quando si genera un client con metodi Async non includeranno i token di cancellazione. In effetti, come mostra l'esempio, e come ha notato anche Stephen Cleary, si presume che le attività sincrone di lunga durata abbiano un modo per annullarle.
kjbartel,

1
Ecco perché ho detto "quando possibile". Principalmente volevo solo menzionare questo avvertimento in modo che le persone che trovavano questa risposta in seguito non avessero l'impressione sbagliata.
Miral,

@Miral Grazie. Ho aggiornato per riflettere questo avvertimento.
kjbartel,

Purtroppo questo non funziona con metodi come "NetworkStream.WriteAsync".
Zeokat,

6

Voglio solo aggiungere la risposta già accettata. Ero bloccato su questo, ma stavo seguendo un percorso diverso nella gestione dell'evento completo. Invece di eseguire wait, aggiungo un gestore completo all'attività.

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

Dove il gestore dell'evento si presenta così

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

Con questo percorso, tutta la gestione è già stata eseguita per te, quando l'attività viene annullata, viene attivato il gestore eventi e puoi vedere se è stato annullato lì.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.