Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Elegantly handle task cancellation

When using tasks for large/long running workloads that I need to be able to cancel I often use a template similar to this for the action the task executes:

public void DoWork(CancellationToken cancelToken) {     try     {         //do work         cancelToken.ThrowIfCancellationRequested();         //more work     }     catch (OperationCanceledException)     {         throw;     }     catch (Exception ex)     {         Log.Exception(ex);         throw;     } } 

The OperationCanceledException should not be logged as an error but must not be swallowed if the task is to transition into the cancelled state. Any other exceptions do not need to be dealt with beyond the scope of this method.

This always felt a bit clunky, and visual studio by default will break on the throw for OperationCanceledException (though I have 'break on User-unhandled' turned off now for OperationCanceledException because of my use of this pattern).

UPDATE: It's 2021 and C#9 gives me the syntax I always wanted:

public void DoWork(CancellationToken cancelToken) {     try     {         //do work         cancelToken.ThrowIfCancellationRequested();         //more work     }     catch (Exception ex) when (ex is not OperationCanceledException)     {         Log.Exception(ex);         throw;     } } 
Ideally I think I'd like to be able to do something like this:
public void DoWork(CancellationToken cancelToken) {     try     {         //do work         cancelToken.ThrowIfCancellationRequested();         //more work     }     catch (Exception ex) exclude (OperationCanceledException)     {         Log.Exception(ex);         throw;     } } 
i.e. have some sort of exclusion list applied to the catch but without language support that is not currently possible (@eric-lippert: c# vNext feature :)).

Another way would be through a continuation:

public void StartWork() {     Task.Factory.StartNew(() => DoWork(cancellationSource.Token), cancellationSource.Token)         .ContinueWith(t => Log.Exception(t.Exception.InnerException), TaskContinuationOptions.OnlyOnFaulted | TaskContinuationOptions.ExecuteSynchronously); }  public void DoWork(CancellationToken cancelToken) {     //do work     cancelToken.ThrowIfCancellationRequested();     //more work } 

but I don't really like that as the exception technically could have more than a single inner exception and you don't have as much context while logging the exception as you would in the first example (if I was doing more than just logging it).

I understand this is a bit of a question of style, but wondering if anyone has any better suggestions?

Do I just have to stick with example 1?

like image 664
Eamon Avatar asked Sep 28 '12 05:09

Eamon


People also ask

How do you handle a cancellation token?

The wait handle of the cancellation token will become signaled in response to a cancellation request, and the method can use the return value of the WaitAny method to determine whether it was the cancellation token that signaled. The operation can then just exit, or throw a OperationCanceledException, as appropriate.

What is the way to create a task that can be Cancelled after it starts?

Create and start a cancelable task. Pass a cancellation token to your user delegate and optionally to the task instance. Notice and respond to the cancellation request in your user delegate. Optionally notice on the calling thread that the task was canceled.

Does CancellationToken throw exception?

An exception thrown by this operation represents cancellation only when its type inherits from OperationCanceledException and when the CancellationToken. IsCancellationRequested property is true .

What is Operation Cancelled exception?

The exception that is thrown in a thread upon cancellation of an operation that the thread was executing.


2 Answers

So, what's the problem? Just throw away catch (OperationCanceledException) block, and set proper continuations:

var cts = new CancellationTokenSource(); var task = Task.Factory.StartNew(() =>     {         var i = 0;         try         {             while (true)             {                 Thread.Sleep(1000);                  cts.Token.ThrowIfCancellationRequested();                  i++;                  if (i > 5)                     throw new InvalidOperationException();             }         }         catch         {             Console.WriteLine("i = {0}", i);             throw;         }     }, cts.Token);  task.ContinueWith(t =>          Console.WriteLine("{0} with {1}: {2}",              t.Status,              t.Exception.InnerExceptions[0].GetType(),              t.Exception.InnerExceptions[0].Message         ),          TaskContinuationOptions.OnlyOnFaulted);  task.ContinueWith(t =>          Console.WriteLine(t.Status),          TaskContinuationOptions.OnlyOnCanceled);  Console.ReadLine();  cts.Cancel();  Console.ReadLine(); 

TPL distinguishes cancellation and fault. Hence, cancellation (i.e. throwing OperationCancelledException within task body) is not a fault.

The main point: do not handle exceptions within task body without re-throwing them.

like image 142
Dennis Avatar answered Sep 20 '22 13:09

Dennis


Here is how you elegantly handle Task cancellation:

Handling "fire-and-forget" Tasks

var cts = new CancellationTokenSource( 5000 );  // auto-cancel in 5 sec. Task.Run( () => {     cts.Token.ThrowIfCancellationRequested();      // do background work      cts.Token.ThrowIfCancellationRequested();      // more work  }, cts.Token ).ContinueWith( task => {     if ( !task.IsCanceled && task.IsFaulted )   // suppress cancel exception         Logger.Log( task.Exception );           // log others } ); 

Handling await Task completion / cancellation

var cts = new CancellationTokenSource( 5000 ); // auto-cancel in 5 sec. var taskToCancel = Task.Delay( 10000, cts.Token );    // do work  try { await taskToCancel; }           // await cancellation catch ( OperationCanceledException ) {}    // suppress cancel exception, re-throw others 
like image 38
Casey Anderson Avatar answered Sep 19 '22 13:09

Casey Anderson