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; } }
public void DoWork(CancellationToken cancelToken) { try { //do work cancelToken.ThrowIfCancellationRequested(); //more work } catch (Exception ex) exclude (OperationCanceledException) { Log.Exception(ex); throw; } }
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?
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.
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.
An exception thrown by this operation represents cancellation only when its type inherits from OperationCanceledException and when the CancellationToken. IsCancellationRequested property is true .
The exception that is thrown in a thread upon cancellation of an operation that the thread was executing.
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.
Here is how you elegantly handle Task cancellation:
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 } );
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With