/.NET

The task mechanism in C# is a powerful beast in the area of parallel and concurrent programming. Controlling this beast may take lots of effort and pain. .NET Framework 4 introduces a convenient approach for cancellation of asynchronous operations.

How do we control

Why should we control the execution flow of the tasks? One reason is business logic or algorithm requirements. Another reason comes from an idea, that there should be no unobserved tasks. Such tasks may hold thread pool and CPU resources and therefore be dangerous. Last but not least is the ability to differentiate being canceled by manually thrown exception or failing by any other exception.

Just for this .NET provides us with two classes:

CancellationTokenSource – an object responsible for creating a cancellation token and sending a cancellation request to all copies of that token.

– an object responsible for creating a cancellation token and sending a cancellation request to all copies of that token. CancellationToken – a structure used by listeners to monitor token current state.

How do we pass token

Firstly, we should somehow make a task use created token. One way is to pass it as an argument to the method responsible for creating the task.

public void CreateCancelledTask() { CancellationTokenSource tokenSource = new CancellationTokenSource(); CancellationToken token = tokenSource.Token; tokenSource.Cancel(); Task.Run(() => Console.WriteLine("Hello from Task"), token); } 1 2 3 4 5 6 7 public void CreateCancelledTask ( ) { CancellationTokenSource tokenSource = new CancellationTokenSource ( ) ; CancellationToken token = tokenSource . Token ; tokenSource . Cancel ( ) ; Task . Run ( ( ) = > Console . WriteLine ( "Hello from Task" ) , token ) ; }

With such an approach, the token is only checked at the very beginning, before starting the execution of lambda expression. If token canceled then the task is also put in canceled mode. In the example above message won’t be printed because token had been canceled before the task was created.

But what if we want to stop task execution in a particular moment? Then you should observe the cancellation token state manually inside the task delegate. There are generally two ways for passing token inside task delegate.

The first way is to make the token variable visible by task delegate. It can be accomplished by using class field, property or even capturing variable within a method.

public void CreateCancellableTask() { var token = tokenSource.Token; Task.Run(() => { var capturedToken = token; // long running operation }, token); <span style="font-weight: 400;">}</span> 1 2 3 4 5 6 7 8 9 10 public void CreateCancellableTask ( ) { var token = tokenSource . Token ; Task . Run ( ( ) = > { var capturedToken = token ; // long running operation } , token ) ; < span style = "font-weight: 400;" > } < / span >

The second way is to pass the token as state object and downcast it inside task delegate.

public void CreateCancellableTaskWithState() { var token = tokenSource.Token; Task.Factory.StartNew(stateObject => { var castedToken = (CancellationToken) stateObject; // long running operation }, token, token); } 1 2 3 4 5 6 7 8 9 10 public void CreateCancellableTaskWithState ( ) { var token = tokenSource . Token ; Task . Factory . StartNew ( stateObject = > { var castedToken = ( CancellationToken ) stateObject ; // long running operation } , token , token ) ; }

How do we observe token

Now when we know how to access token inside a task, it is time to become acquainted with how we can observe token state. You can observe it in three ways: by polling, using callback registration and via wait handle.

1. By polling – periodically check IsCancellationRequested property

Task.Run(() => { // polling boolean property while (true) { token.ThrowIfCancellationRequested(); // throw OperationCancelledException if requested // do some work } // release resources and exit }, token); 1 2 3 4 5 6 7 8 9 10 Task . Run ( ( ) = > { // polling boolean property while ( true ) { token . ThrowIfCancellationRequested ( ) ; // throw OperationCancelledException if requested // do some work } // release resources and exit } , token ) ;

2. Callback registration – provide a callback that would be executed right after cancellation is requested

Task.Run(async () => { var webClient = newWebClient(); // registering callback that would cancel downloading token.Register(() => webClient.CancelAsync()); var data = await webClient.DownloadDataTaskAsync("http://www.google.com/"); }, token); 1 2 3 4 5 6 7 Task . Run ( async ( ) = > { var webClient = newWebClient ( ) ; // registering callback that would cancel downloading token . Register ( ( ) = > webClient . CancelAsync ( ) ) ; var data = await webClient . DownloadDataTaskAsync ( "http://www.google.com/" ) ; } , token ) ;

3. Waiting for the wait handle provided by CancellationToken

var autoResetEvent = newAutoResetEvent(false); // some code... Task.Run(() => { var handleArray = new[] { autoResetEvent, token.WaitHandle }; // waiting on wait handle to signal first var finishedId = WaitHandle.WaitAny(handleArray); if (finishedId == Array.IndexOf(handleArray, token.WaitHandle)) { // if token wait handle was first to signal then exit return; } // continue working... }, token); 1 2 3 4 5 6 7 8 9 10 11 12 13 14 var autoResetEvent = newAutoResetEvent ( false ) ; // some code... Task . Run ( ( ) = > { var handleArray = new [ ] { autoResetEvent , token . WaitHandle } ; // waiting on wait handle to signal first var finishedId = WaitHandle . WaitAny ( handleArray ) ; if ( finishedId == Array . IndexOf ( handleArray , token . WaitHandle ) ) { // if token wait handle was first to signal then exit return ; } // continue working... } , token ) ;

You can also observe multiple tokens simultaneously, by creating a linked token source that can join multiple tokens into one

public CancellationToken LinkTokens(CancellationToken firstToken, CancellationToken secondToken) { CancellationTokenSource linkedToukenSource = CancellationTokenSource.CreateLinkedTokenSource(firstToken, secondToken); CancellationToken lindkedToken = linkedToukenSource.Token; return linkedToken; } 1 2 3 4 5 6 7 8 public CancellationToken LinkTokens ( CancellationToken firstToken , CancellationToken secondToken ) { CancellationTokenSource linkedToukenSource = CancellationTokenSource . CreateLinkedTokenSource ( firstToken , secondToken ) ; CancellationToken lindkedToken = linkedToukenSource . Token ; return linkedToken ; }

How do we cancel

We already know how to pass and observe tokens. What is left is how we send the cancellation request. For canceling, we use CancellationTokenSource object. Cancellation request may be direct or deferred.

// #1 create a token source with timeout tokenSource = newCancellationTokenSource(TimeSpan.FromSeconds(2)); // #2 request cancellation after timeout tokenSource.CancelAfter(TimeSpan.FromSeconds(2)); // #3 directly request cancellation tokenSource.Cancel(); // or tokenSource.Cancel(true); 1 2 3 4 5 6 7 8 9 10 11 // #1 create a token source with timeout tokenSource = newCancellationTokenSource ( TimeSpan . FromSeconds ( 2 ) ) ; // #2 request cancellation after timeout tokenSource . CancelAfter ( TimeSpan . FromSeconds ( 2 ) ) ; // #3 directly request cancellation tokenSource . Cancel ( ) ; // or tokenSource . Cancel ( true ) ;

When we use the Cancel method directly, it implicitly calls all registered callbacks on the current thread. If callbacks throw exceptions, they are wrapped in AggregateException object that will be propagated then to the caller.

Since callbacks are called one by one, optional boolean argument for Cancel method lets you specify behavior upon encountering an exception in any of the registered callbacks.

If the argument is true then the exception will immediately propagate out of the call to Cancel, preventing the remaining callbacks from executing.

If the argument is false, all registered callbacks will be executed with all thrown exceptions wrapped into the AggregateException.

After detecting cancellation request inside a task, common approach is to do some cleanup and throw OperationCanceledException object, to put the task in a canceled state.

public void CancelAndLogMessage() { Task.Run(() => { while (true) { // do some work if (token.IsCancellationRequested) { Console.WriteLine(“Operation is going to be cancelled”); // do some clean up throw new OperationCancelledException(); } } }, token); 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public void CancelAndLogMessage ( ) { Task . Run ( ( ) = > { while ( true ) { // do some work if ( token . IsCancellationRequested ) { Console . WriteLine ( “ Operation is going to be cancelled ” ) ; // do some clean up throw new OperationCancelledException ( ) ; } } } , token ) ;

Conclusion

The task cancellation mechanism is an easy and useful tool for controlling task execution flow. It is a part of big .NET Framework ecosystem, therefore its usage in most cases is more preferable than some custom solutions. Even if you don’t want to implement your own cancellation logic you still have to pass CancellationToken to all async methods that support cancellation (i.e HttpClient.GetAsync() or DbSet.FindAsync()).