2015-12-15 15 views
8

Nella nostra applicazione lavoriamo molto con async/await e Task. Pertanto utilizza Task.Run molto, a volte con supporto per l'annullamento utilizzando il built-in CancellationToken.TaskCancellationException come evitare l'eccezione sul flusso di controllo di successo?

public Task DoSomethingAsync(CancellationToken cancellationToken) 
{ 
    return Task.Run(() => 
    { 
     while (true) 
     { 
      if (cancellationToken.IsCancellationRequested) break; 
      //do some work 
     } 
    }, cancellationToken); 
} 

se lo faccio ora annullare l'esecuzione utilizzando il CancellationToken l'esecuzione si ferma all'inizio del ciclo successivo, o se il compito non è stato avviato a tutti viene generata un'eccezione (TaskCanceledException all'interno Task.Run). La domanda è ora perché Task.Run utilizza un'eccezione per controllare l'annullamento con esito positivo invece di restituire solo un'attività completata. C'è qualche ragione specifica per cui MS non si è attenuta alla regola "NON usare le eccezioni per controllare il flusso di esecuzione" ?.

E come posso evitare il Boxing di ogni metodo che supporta la cancellazione (che è molto) in un blocco try catch (TaskCancelledException) completamente inutile?

+2

" Cancellazione! = Completamento riuscito ". Cosa ti aspetteresti 'Task ' per tornare quando viene cancellato? –

+0

Un'istanza dell'attività annullata con ** cancellationTokenSource.Cancel(); ** avrà lo ** stato TaskStatus.RanToCompletion **, non lo stato ** TaskStatus.Canceled **. –

+0

Hai ragione per le attività con valore di ritorno è necessaria l'eccezione. Non ci ho pensato. – Console

risposta

5

Bene, non si può davvero vedere la differenza nel proprio scenario molto semplice: non si sta effettivamente utilizzando il risultato dello Task e non è necessario propagare l'annullamento attraverso uno stack di chiamate complesso.

In primo luogo, il tuo Task potrebbe restituire un valore. Cosa restituisci quando l'operazione è stata annullata?

In secondo luogo, potrebbero esserci altre attività che seguono l'attività annullata. Probabilmente vuoi propagare la cancellazione attraverso gli altri compiti a tuo piacimento.

Le eccezioni si propagano. L'annullamento dell'attività è praticamente identico a Thread.Abort in questo utilizzo: quando si emette uno Thread.Abort, viene utilizzato uno ThreadAbortException per assicurarsi di tornare indietro fino all'inizio. Altrimenti, tutti i tuoi metodi dovrebbero controllare il risultato di ogni metodo che chiamano, controllare se sono stati cancellati e restituire se stessi se necessario - e abbiamo già visto che le persone ignoreranno i valori di ritorno degli errori nella vecchia scuola C :)

Alla fine, l'annullamento dell'attività, proprio come l'interruzione di thread, è uno scenario eccezionale. Comporta già sincronizzazione, stack unwinding ecc.

Tuttavia, questo non significa che devi necessariamente utilizzare try-catch per rilevare l'eccezione: è possibile utilizzare gli stati di attività. Ad esempio, è possibile utilizzare una funzione di supporto come questo:

public static Task<T> DefaultIfCanceled<T>(this Task<T> @this, T defaultValue = default(T)) 
{ 
    return 
    @this.ContinueWith 
     (
     t => 
     { 
      if (t.IsCanceled) return defaultValue; 

      return t.Result; 
     } 
    ); 
} 

che si può usare come

await SomeAsync().DefaultIfCanceled(); 

Naturalmente, si deve rilevare che nessuno ti costringe ad utilizzare questo metodo di cancellazione - è semplicemente fornito per comodità. Ad esempio, è possibile utilizzare il proprio tipo amplificato per conservare le informazioni di cancellazione e gestire manualmente la cancellazione. Ma quando inizi a farlo, troverai il motivo per cui la cancellazione viene gestita usando le eccezioni: fare questo in un codice imperativo è un problema, quindi perderai un sacco di sforzi senza alcun guadagno, o passerai a un modo più funzionale di programmazione (vieni, abbiamo i cookie! *).

(*) Declinazione di responsabilità: in realtà non abbiamo i cookie. Ma puoi crearne uno tuo!

0

L'eccezione è lanciata per uno scopo come altri nella comunità hanno già sottolineato.

Tuttavia, se si desidera avere un maggiore controllo sul TaskCanceledException comportamento e avere ancora la logica isolato in un solo luogo si può implementare un metodo di estensione per estendere Task che gestisce la cancellazione, qualcosa di simile -

public async Task DoSomethingAsync(CancellationToken cancellationToken) 
    { 
     await Task.Run(() => 
     { 
      while (true) 
      { 
       if (cancellationToken.IsCancellationRequested) break; 
       //do some work 
      } 
     }). 
     WithCancellation(cancellationToken,false); // pass the cancellation token to extension funciton instead to run 
    } 



static class TaskCacellationHelper 
{ 
    private struct Void { } // just to support TaskCompletionSource class. 


    public static async Task WithCancellation(this Task originalTask, CancellationToken ct, bool suppressCancellationExcetion) 
    { 
     // Create a Task that completes when the CancellationToken is canceled 
     var cancelTask = new TaskCompletionSource<Void>(); 
     // When the CancellationToken is canceled, complete the Task 
     using (ct.Register(
     t => ((TaskCompletionSource<Void>)t).TrySetResult(new Void()), cancelTask)) 
     { 
      // Create a Task that completes when either the original or 
      // CancellationToken Task completes 
      Task any = await Task.WhenAny(originalTask, cancelTask.Task); 
      // If any Task completes due to CancellationToken, throw OperationCanceledException 
      if (any == cancelTask.Task) 
      { 
       // 
       if (suppressCancellationExcetion == false) 
       { 
        ct.ThrowIfCancellationRequested(); 
       } 
       else 
       { 
        Console.WriteLine("Cancelled but exception supressed"); 
       } 
      } 
     } 
     // await original task. Incase of cancellation your logic will break the while loop    
     await originalTask; 
    } 
} 
Problemi correlati