2015-12-18 15 views
5

Il codice seguente crea un'attività che viene annullata. L'espressione await (caso 1) genera System.OperationCanceledException mentre sincrono Wait() (caso 2) genera System.Threading.Tasks.TaskCanceledException (inserito in System.AggregateException).OperationCanceledException VS TaskCanceledException quando l'attività viene annullata

using System; 
using System.Threading; 
using System.Threading.Tasks; 

public class Program 
{ 
    public static void Main() 
    { 
     Program.MainAsync().Wait(); 
    } 

    private static async Task MainAsync() 
    { 
     using(var cancellationTokenSource = new CancellationTokenSource()) 
     { 
      var token = cancellationTokenSource.Token; 
      const int cancelationCheckTimeout = 100; 

      var task = Task.Run(
       async() => 
       { 
        for (var i = 0; i < 100; i++) 
        { 
         token.ThrowIfCancellationRequested(); 
         Console.Write("."); 
         await Task.Delay(cancelationCheckTimeout); 
        } 
       }, 
       cancellationTokenSource.Token 
      ); 

      var cancelationDelay = 10 * cancelationCheckTimeout; 
      cancellationTokenSource.CancelAfter(cancelationDelay); 

      try 
      { 
       await task; // (1) 
       //task.Wait(); // (2) 
      } 
      catch(Exception ex) 
      { 
       Console.WriteLine(ex.ToString()); 
       Console.WriteLine($"Task.IsCanceled: {task.IsCanceled}"); 
       Console.WriteLine($"Task.IsFaulted: {task.IsFaulted}"); 
       Console.WriteLine($"Task.Exception: {((task.Exception == null) ? "null" : task.Exception.ToString())}"); 
      } 
     } 
    } 
} 

Caso 1 uscita:

..........System.OperationCanceledException: The operation was canceled. 
    at System.Threading.CancellationToken.ThrowIfCancellationRequested() 
    at Program.<>c__DisplayClass1_0.<<MainAsync>b__0>d.MoveNext() 
--- End of stack trace from previous location where exception was thrown --- 
    at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() 
    at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) 
    at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) 
    at Program.<MainAsync>d__1.MoveNext() 
Task.IsCanceled: True 
Task.IsFaulted: False 
Task.Exception: null 

Caso 2 uscita:

..........System.AggregateException: One or more errors occurred. ---> System.Threading.Tasks.TaskCanceledException: A task was canceled. 
    --- End of inner exception stack trace --- 
    at System.Threading.Tasks.Task.Wait(Int32 millisecondsTimeout, CancellationToken cancellationToken) 
    at System.Threading.Tasks.Task.Wait() 
    at Program.<MainAsync>d__1.MoveNext() 
---> (Inner Exception #0) System.Threading.Tasks.TaskCanceledException: A task was canceled.<--- 

Task.IsCanceled: True 
Task.IsFaulted: False 
Task.Exception: null 

Perché System.AggregateException nel secondo caso non contiene System.OperationCanceledException come eccezione interna?

so che ThrowIfCancellationRequested() tiri OperationCanceledException e possiamo vedere che in entrambi i casi Task arriva a stato annullato (non guasta).

Questo mi lascia perplesso perché la cancellazione di un metodo da API .NET produce un comportamento coerente in entrambi i casi - attività annullata getta solo TaskCanceledException:

using System; 
using System.Threading; 
using System.Threading.Tasks; 

public class Program 
{ 
    public static void Main() 
    { 
     Program.MainAsync().Wait(); 
    } 

    private static async Task MainAsync() 
    { 
     using(var cancellationTokenSource = new CancellationTokenSource()) 
     { 
      var token = cancellationTokenSource.Token; 

      var task = Task.Delay(1000, token); 
      cancellationTokenSource.CancelAfter(100); 

      try 
      { 
       await task; // (1) 
       //task.Wait(); // (2) 
      } 
      catch(Exception ex) 
      { 
       Console.WriteLine(ex.ToString()); 
       Console.WriteLine($"Task.IsCanceled: {task.IsCanceled}"); 
       Console.WriteLine($"Task.IsFaulted: {task.IsFaulted}"); 
       Console.WriteLine($"Task.Exception: {((task.Exception == null) ? "null" : task.Exception.ToString())}"); 
      } 
     } 
    } 
} 

Caso 1 uscita:

System.Threading.Tasks.TaskCanceledException: A task was canceled. 
    at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) 
    at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) 
    at Program.<MainAsync>d__1.MoveNext() 
Task.IsCanceled: True 
Task.IsFaulted: False 
Task.Exception: null 

Caso 2 uscita:

System.AggregateException: One or more errors occurred. ---> System.Threading.Tasks.TaskCanceledException: A task was canceled. 
    --- End of inner exception stack trace --- 
    at System.Threading.Tasks.Task.Wait(Int32 millisecondsTimeout, CancellationToken cancellationToken) 
    at System.Threading.Tasks.Task.Wait() 
    at Program.<MainAsync>d__1.MoveNext() 
---> (Inner Exception #0) System.Threading.Tasks.TaskCanceledException: A task was canceled.<--- 

Task.IsCanceled: True 
Task.IsFaulted: False 
Task.Exception: null 
+0

Ha importanza? 'TaskCanceledException' è una classe derivata di' OperationCanceledException' quindi se hai fatto 'catch (OperationCanceledException e)' avresti catturato entrambi i tipi di eccezioni. L'unica informazione che perdi è la proprietà 'TaskCanceledException.Task'. –

+0

Sono d'accordo ma ero più curioso dei motivi per introdurre/utilizzare 'TaskCanceledException' quando c'è già' OperationCanceledException'. –

+1

Il motivo per introdurlo è quando si hanno alcuni casi in cui si è in grado di afferrare il contesto dell'attività che è stata annullata, quindi in quelle situazioni in cui è possibile si genera la forma più derivata che include l'attività.'ThrowIfCancellationRequested' è scritto genericamente, non sa che si trova all'interno di un Task, quindi solleva l'eccezione più generale senza la proprietà Task. –

risposta

5

La differenza deriva dall'utilizzo di token.ThrowIfCancellationRequested(). Questo metodo controlla la cancellazione e, se richiesto, genera OperationCanceledException in modo specifico e non TaskCanceledException (comprensibile come CancellationToken non è esclusivo per il TPL). Potete guardare il reference source e vedere che chiama questo metodo:

private void ThrowOperationCanceledException() 
{ 
    throw new OperationCanceledException(Environment.GetResourceString("OperationCanceled"), this); 
} 

cancellazione "regolare", anche se sarà davvero generare un TaskCanceledException. Si può vedere che cancellando il token prima che il compito avuto la possibilità di iniziare a correre:

cancellationTokenSource.Cancel(); 
var task = Task.Run(() => { }, cancellationTokenSource.Token); 
try 
{ 
    await task; 
} 
catch (Exception ex) 
{ 
    Console.WriteLine(ex.ToString()); 
    Console.WriteLine($"Task.IsCanceled: {task.IsCanceled}"); 
    Console.WriteLine($"Task.IsFaulted: {task.IsFaulted}"); 
    Console.WriteLine($"Task.Exception: {((task.Exception == null) ? "null" : task.Exception.ToString())}"); 
} 

uscita:

System.Threading.Tasks.TaskCanceledException: A task was canceled. 
    at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) 
    at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) 
    at System.Runtime.CompilerServices.TaskAwaiter.GetResult() 
    at Sandbox.Program.<MainAsync>d__1.MoveNext() 
Task.IsCanceled: True 
Task.IsFaulted: False 
Task.Exception: null 

metodi tradizionali .Net di solito non utilizzare CancellationToken.ThrowIfCancellationRequested per l'API asincrona come è appropriato quando si scarica il lavoro su un altro thread. Questi metodi sono per le operazioni intrinsecamente asincrone, quindi la cancellazione viene monitorata utilizzando CancellationToken.Register (o lo InternalRegisterWithoutEC interno).

+0

Ho osservato la cancellazione dei metodi .NET (ad esempio 'Task.Delay()' o 'FileStream.WriteAsync()') e ho notato che tutti lanciano 'TaskCanceledException' se cancellati (tramite CancellationToken che non è esclusivo del TPL come hai detto tu) . Ha senso che cancellare l'attività stessa (non la callback che esegue) genera 'TaskCanceledException' come nell'esempio precedente. Ma perché 'FileStream.WriteAsync()' non lancia 'OperationCanceledException'? Potresti ampliare il ragionamento di 'offloading work ad un altro thread '? –

+1

@BojanKomazec 'ThrowIfCancellationRequested' lancia' OperationCanceledException'. Si chiama quel metodo solo quando si ha il codice in esecuzione che periodicamente deve verificare la cancellazione. L'API asincrona integrata non lo fa, poiché è sincrono. Non contiene un thread che fa qualcosa in un ciclo. Quindi non c'è codice in esecuzione per chiamare 'ThrowIfCancellationRequested'. – i3arnon

+1

@BojanKomazec 'Task.Delay', ad esempio, crea un'attività promessa, avvia un timer che completerà l'attività quando termina il timeout e registra un delegato per annullare l'attività sul token con' CancellationToken.Register'. Quindi restituisce l'attività e il gioco è fatto. Non c'è un posto dove chiamare "ThrowIfCancellationRequested". – i3arnon