Cancellation - Concurrency in C# Cookbook (2014)

Concurrency in C# Cookbook (2014)

Chapter 9. Cancellation

The .NET 4.0 framework introduced exhaustive and well-designed cancellation support. This support is cooperative, which means that cancellation can be requested but not enforced on code. Since cancellation is cooperative, it is not possible to cancel code unless it is written to support cancellation. For this reason, I recommend supporting cancellation in as much of your own code as possible.

Cancellation is a type of signal, with two different sides: a source that triggers the cancellation, and a receiver that responds to the cancellation. In .NET, the source is CancellationTokenSource and the receiver is CancellationToken. The recipes in this chapter will cover both sides of cancellation in normal usage and describe how to use the cancellation support to interoperate with nonstandard forms of cancellation.

Cancellation is treated as a special kind of error. The convention is that canceled code will throw an exception of type OperationCanceledException (or a derived type, such as TaskCanceledException). This way the calling code knows that the cancellation was observed.

To indicate to calling code that your method supports cancellation, you should take a CancellationToken as a parameter. This parameter is usually the last parameter, unless your method also reports progress (Recipe 2.3). You can also consider providing an overload or default parameter value for consumers that do not require cancellation:

public void CancelableMethodWithOverload(CancellationToken cancellationToken)

{

// code goes here

}

public void CancelableMethodWithOverload()

{

CancelableMethodWithOverload(CancellationToken.None);

}

public void CancelableMethodWithDefault(

CancellationToken cancellationToken = default(CancellationToken))

{

// code goes here

}

CancellationToken.None is a special value that is equivalent to default(CancellationToken) and represents a cancellation token that will never be canceled. Consumers pass this value when they don’t ever want the operation to be canceled.

9.1. Issuing Cancellation Requests

Problem

You have cancelable code (that takes a CancellationToken) and you need to cancel it.

Solution

The CancellationTokenSource type is the source for a CancellationToken. The CancellationToken only enables code to respond to cancellation requests; the CancellationTokenSource members allow code to request cancellation.

Each CancellationTokenSource is independent from every other CancellationTokenSource (unless you link them, which we will consider in Recipe 9.8). The Token property returns a CancellationToken for that source, and the Cancel method issues the actual cancellation request.

The following code illustrates creating a CancellationTokenSource and using Token and Cancel. This code uses an async method because it’s easier to illustrate in a short code sample; the same Token/Cancel pair is used to cancel all kinds of code:

void IssueCancelRequest()

{

var cts = new CancellationTokenSource();

var task = CancelableMethodAsync(cts.Token);

// At this point, the operation has been started.

// Issue the cancellation request.

cts.Cancel();

}

In the example code above, the task variable is ignored after it has started running; in real-world code, that task would probably be stored somewhere and awaited so that the end user is aware of the final result.

When you cancel code, there is almost always a race condition. The cancelable code may have been just about to finish when the cancel request is made, and if it doesn’t happen to check its cancellation token before finishing, it will actually complete successfully. In fact, when you cancel code, there are three possibilities: it may respond to the cancellation request (throwing OperationCanceledException), it may finish successfully, or it may finish with an error unrelated to the cancellation (throwing a different exception).

The following code is just like the last, except that it awaits the task, illustrating all three possible results:

async Task IssueCancelRequestAsync()

{

var cts = new CancellationTokenSource();

var task = CancelableMethodAsync(cts.Token);

// At this point, the operation is happily running.

// Issue the cancellation request.

cts.Cancel();

// (Asynchronously) wait for the operation to finish.

try

{

await task;

// If we get here, the operation completed successfully

// before the cancellation took effect.

}

catch (OperationCanceledException)

{

// If we get here, the operation was canceled before it completed.

}

catch (Exception)

{

// If we get here, the operation completed with an error

// before the cancellation took effect.

throw;

}

}

Normally, setting up the CancellationTokenSource and issuing the cancellation are in separate methods. Once you cancel a CancellationTokenSource instance, it is permanently canceled. If you need another source, you’ll need to create another instance. The following code is a more realistic GUI-based example that uses one button to start an asynchronous operation and another button to cancel it. It also disables and enables the “start” and “cancel” buttons so that there can only be one operation at a time:

private CancellationTokenSource _cts;

private async void StartButton_Click(object sender, RoutedEventArgs e)

{

StartButton.IsEnabled = false;

CancelButton.IsEnabled = true;

try

{

_cts = new CancellationTokenSource();

var token = _cts.Token;

await Task.Delay(TimeSpan.FromSeconds(5), token);

MessageBox.Show("Delay completed successfully.");

}

catch (OperationCanceledException)

{

MessageBox.Show("Delay was canceled.");

}

catch (Exception)

{

MessageBox.Show("Delay completed with error.");

throw;

}

finally

{

StartButton.IsEnabled = true;

CancelButton.IsEnabled = false;

}

}

private void CancelButton_Click(object sender, RoutedEventArgs e)

{

_cts.Cancel();

}

Discussion

The most realistic example in this recipe used a GUI application, but don’t get the impression that cancellation is just for user interfaces. Cancellation has its place on the server as well; for example, ASP.NET provides a cancellation token representing the request timeout. It’s true that cancellation token sources are rarer on the server side, but there’s no reason you can’t use them; I have used a CancellationTokenSource to request cancellation when ASP.NET decides to unload the app domain.

See Also

Recipe 9.4 covers passing tokens to async code.

Recipe 9.5 covers passing tokens to parallel code.

Recipe 9.6 covers using tokens with reactive code.

Recipe 9.7 covers passing tokens to dataflow meshes.

9.2. Responding to Cancellation Requests by Polling

Problem

You have a loop in your code that needs to support cancellation.

Solution

When you have a processing loop in your code, then there isn’t a lower-level API to which you can pass the CancellationToken. In this case, you should periodically check whether the token has been canceled. The following code observes the token periodically while executing a CPU-bound loop:

public int CancelableMethod(CancellationToken cancellationToken)

{

for (int i = 0; i != 100; ++i)

{

Thread.Sleep(1000); // Some calculation goes here.

cancellationToken.ThrowIfCancellationRequested();

}

return 42;

}

If your loop is very tight (i.e., if the body of your loop executes very quickly), then you may want to limit how often you check your cancellation token. As always, measure your performance before and after a change like this before deciding which way is best. The following code is similar to the previous example, but it has more iterations of a faster loop, so I added a limit to how often the token is checked:

public int CancelableMethod(CancellationToken cancellationToken)

{

for (int i = 0; i != 100000; ++i)

{

Thread.Sleep(1); // Some calculation goes here.

if (i % 1000 == 0)

cancellationToken.ThrowIfCancellationRequested();

}

return 42;

}

The proper limit to use depends entirely on how much work you’re doing and how responsive the cancellation needs to be.

Discussion

The majority of the time, your code should just pass through the CancellationToken to the next layer. We’ll look at examples of this in Recipe 9.4, Recipe 9.5, Recipe 9.6, and Recipe 9.7. This polling recipe should only be used if you have a processing loop that needs to support cancellation.

There is another member on CancellationToken called IsCancellationRequested, which starts returning true when the token is canceled. Some people use this member to respond to cancellation, usually by returning a default or null value. However, I do not recommend that approach for most code. The standard cancellation pattern is to raise an OperationCanceledException, which is taken care of by ThrowIfCancellationRequested. If code further up the stack wants to catch the exception and act like the result is null, then that’s fine, but any code taking a CancellationToken should follow the standard cancellation pattern. If you do decide not to follow the cancellation pattern, at least document it clearly.

ThrowIfCancellationRequested works by polling the cancellation token; your code has to call it at regular intervals. There is also a way to register a callback that is invoked when cancellation is requested. The callback approach is more about interoperating with other cancellation systems, so we’ll cover that in Recipe 9.9.

See Also

Recipe 9.4 covers passing tokens to async code.

Recipe 9.5 covers passing tokens to parallel code.

Recipe 9.6 covers using tokens with reactive code.

Recipe 9.7 covers passing tokens to dataflow meshes.

Recipe 9.9 covers using callbacks instead of polling to respond to cancellation requests.

Recipe 9.1 covers issuing a cancellation request.

9.3. Canceling Due to Timeouts

Problem

You have some code that needs to stop running after a timeout.

Solution

Cancellation is a natural solution for timeout situations. A timeout is just one type of cancellation request. The code that needs to be canceled merely observes the cancellation token just like any other cancellation; it should neither know nor care that the cancellation source is a timer.

NET 4.5 introduces some convenience methos for cancellation token sources that automatically issue a cancel request based on a timer. You can pass the timeout into the constructor:

async Task IssueTimeoutAsync()

{

var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));

var token = cts.Token;

await Task.Delay(TimeSpan.FromSeconds(10), token);

}

Alternatively, if you already have a CancellationTokenSource instance, you can start a timeout for that instance:

async Task IssueTimeoutAsync()

{

var cts = new CancellationTokenSource();

var token = cts.Token;

cts.CancelAfter(TimeSpan.FromSeconds(5));

await Task.Delay(TimeSpan.FromSeconds(10), token);

}

The constructor is not available on .NET 4.0, but the CancelAfter method is provided by the Microsoft.Bcl.Async NuGet library for that platform.

Discussion

Whenever you need to execute code with a timeout, you should use CancellationTokenSource and CancelAfter (or the constructor). There are other ways to do the same thing, but using the existing cancellation system is the easiest and most efficient option.

Remember that the code to be canceled needs to observe the cancellation token; it is not possible to easily cancel uncancelable code.

See Also

Recipe 9.4 covers passing tokens to async code.

Recipe 9.5 covers passing tokens to parallel code.

Recipe 9.6 covers using tokens with reactive code.

Recipe 9.7 covers passing tokens to dataflow meshes.

9.4. Canceling async Code

Problem

You are using async code and need to support cancellation.

Solution

The easiest way to support cancellation in asynchronous code is to just pass the CancellationToken through to the next layer. This example code performs an asynchronous delay and then returns a value; it supports cancellation by just passing the token to Task.Delay:

public async Task<int> CancelableMethodAsync(CancellationToken cancellationToken)

{

await Task.Delay(TimeSpan.FromSeconds(2), cancellationToken);

return 42;

}

Many asynchronous APIs support CancellationToken, so enabling cancellation yourself is usually a simple matter of taking a token and passing it along. As a general rule, if your method calls APIs that take CancellationToken, then your method should also take aCancellationToken and pass it to every API that supports it.

Discussion

Unfortunately, there are some methods that do not support cancellation. When you are in this situation, there’s no easy solution. It is not possible to safely stop arbitrary code unless it is wrapped in a separate executable. In this case, you do always have the option of pretending to cancel the operation by ignoring the result.

Cancellation should be provided as an option whenever possible. This is because proper cancellation at a higher level depends on proper cancellation at the lower level. So, when you are writing your own async methods, try your best to include support for cancellation; you never know what higher-level method will want to call yours, and it might need cancellation.

See Also

Recipe 9.1 covers issuing a cancellation request.

Recipe 9.3 covers using cancellation as a timeout.

9.5. Canceling Parallel Code

Problem

You are using parallel code and need to support cancellation.

Solution

The easiest way to support cancellation is to pass the CancellationToken through to the parallel code. Parallel methods support this by taking a ParallelOptions instance. You can set the CancellationToken on a ParallelOptions instance in the following manner:

static void RotateMatrices(IEnumerable<Matrix> matrices, float degrees,

CancellationToken token)

{

Parallel.ForEach(matrices,

new ParallelOptions { CancellationToken = token },

matrix => matrix.Rotate(degrees));

}

Alternatively, it is possible to observe the CancellationToken directly in your loop body:

static void RotateMatrices2(IEnumerable<Matrix> matrices, float degrees,

CancellationToken token)

{

// Warning: not recommended; see below.

Parallel.ForEach(matrices, matrix =>

{

matrix.Rotate(degrees);

token.ThrowIfCancellationRequested();

});

}

However, the alternative method is more work and does not compose as well; with the alternate method, the parallel loop will wrap the OperationCanceledException within an AggregateException. Also, if you pass the CancellationToken as part of a ParallelOptionsinstance, the Parallel class may make more intelligent decisions about how often to check the token. For these reasons, it is best to pass the token as an option.

Parallel LINQ (PLINQ) also has built-in support for cancellation, via the WithCancellation operator:

static IEnumerable<int> MultiplyBy2(IEnumerable<int> values,

CancellationToken cancellationToken)

{

return values.AsParallel()

.WithCancellation(cancellationToken)

.Select(item => item * 2);

}

Discussion

Supporting cancellation for parallel work is important for a good user experience. If your application is doing parallel work, it will use a large amount of CPU at least for a short time. High CPU usage is something that users notice, even if it doesn’t interfere with other applications on the same machine. So, I recommend supporting cancellation whenever you do parallel computation (or any other CPU-intensive work), even if the total time spent with high CPU usage is not extremely long.

See Also

Recipe 9.1 covers issuing a cancellation request.

9.6. Canceling Reactive Code

Problem

You have some reactive code, and you need it to be cancelable.

Solution

The Reactive Extensions library has a notion of a subscription to an observable stream. Your code can dispose of the subscription to unsubscribe from the stream. In many cases, this is sufficient to logically cancel the stream. For example, the following code subscribes to mouse clicks when one button is pressed and unsubscribes (cancels the subscription) when another button is pressed:

private IDisposable _mouseMovesSubscription;

private void StartButton_Click(object sender, RoutedEventArgs e)

{

var mouseMoves = Observable

.FromEventPattern<MouseEventHandler, MouseEventArgs>(

handler => (s, a) => handler(s, a),

handler => MouseMove += handler,

handler => MouseMove -= handler)

.Select(x => x.EventArgs.GetPosition(this));

_mouseMovesSubscription = mouseMoves.Subscribe(val =>

{

MousePositionLabel.Content = "(" + val.X + ", " + val.Y + ")";

});

}

private void CancelButton_Click(object sender, RoutedEventArgs e)

{

if (_mouseMovesSubscription != null)

_mouseMovesSubscription.Dispose();

}

However, it can be really convenient to make Rx work with the CancellationTokenSource/CancellationToken system that everything else uses for cancellation. The rest of this recipe covers ways that Rx interacts with CancellationToken.

The first major use case is when the observable code is wrapped in asynchronous code. We considered this situation in Recipe 7.5, and now we want to add CancellationToken support. In general, the easiest way to do this is to perform all operations using reactive operators and then callToTask to convert the last resulting element to an awaitable task. The following code shows how to asynchronously take the last element in a sequence:

CancellationToken cancellationToken = ...

IObservable<int> observable = ...

int lastElement = await observable.TakeLast(1).ToTask(cancellationToken);

// or: int lastElement = await observable.ToTask(cancellationToken);

Taking the first element is very similar; we just modify the observable before calling ToTask:

CancellationToken cancellationToken = ...

IObservable<int> observable = ...

int firstElement = await observable.Take(1).ToTask(cancellationToken);

Asynchronously converting the entire observable sequence to a task is likewise similar:

CancellationToken cancellationToken = ...

IObservable<int> observable = ...

IList<int> allElements = await observable.ToList().ToTask(cancellationToken);

Finally, let’s consider the reverse situation. We’ve looked at several ways to handle situations where Rx code responds to CancellationToken—that is, where a CancellationTokenSource cancel request is translated into an unsubscription (a Dispose). We can also go the other way: issuing a cancellation request as a response to disposal.

The FromAsync, StartAsync, and SelectMany operators all support cancellation, as we saw in Recipe 7.6. This covers the vast majority of use cases. Rx also provides a CancellationDisposable type, which we can use directly as such:

using (var cancellation = new CancellationDisposable())

{

CancellationToken token = cancellation.Token;

// Pass the token to methods that respond to it.

}

// At this point, the token is canceled.

Discussion

Rx has its own notion of cancellation: unsubscription. This recipe looked at several ways to make Rx play nicely with the universal cancellation framework introduced in .NET 4.0. As long as you are in the Rx world portion of your code, just use the Rx subscription/unsubscription system; it’s cleanest if you only introduce CancellationToken support at the boundaries.

See Also

Recipe 7.5 covers asynchronous wrappers around Rx code (without cancellation support).

Recipe 7.6 covers Rx wrappers around asynchronous code (with cancellation support).

Recipe 9.1 covers issuing a cancellation request.

9.7. Canceling Dataflow Meshes

Problem

You are using dataflow meshes and need to support cancellation.

Solution

The best way to support cancellation in your own code is to pass the CancellationToken through to a cancelable API. Each block in a dataflow mesh supports cancellation as a part of its DataflowBlockOptions. If we want to extend our custom dataflow block with cancellation support, we just set the CancellationToken property on the block options:

IPropagatorBlock<int, int> CreateMyCustomBlock(

CancellationToken cancellationToken)

{

var blockOptions = new ExecutionDataflowBlockOptions

{

CancellationToken = cancellationToken

};

var multiplyBlock = new TransformBlock<int, int>(item => item * 2,

blockOptions);

var addBlock = new TransformBlock<int, int>(item => item + 2,

blockOptions);

var divideBlock = new TransformBlock<int, int>(item => item / 2,

blockOptions);

var flowCompletion = new DataflowLinkOptions

{

PropagateCompletion = true

};

multiplyBlock.LinkTo(addBlock, flowCompletion);

addBlock.LinkTo(divideBlock, flowCompletion);

return DataflowBlock.Encapsulate(multiplyBlock, divideBlock);

}

In the example, I applied the CancellationToken to every block in the mesh. This isn’t strictly necessary. Since I’m also propagating completion along the links, I could just apply it to the first block and allow it to propagate through. Cancellations are considered a special form of error, so the blocks further down the pipeline would be completed with an error as that error propagates through. However, if I am canceling a mesh, I may as well cancel every block simultaneously; so I usually just set the CancellationToken option on every block.

Discussion

In dataflow meshes, cancellation is not a form of flush. When a block is canceled, it drops all its input and refuses to take any new items. So if you cancel a block while it’s running, you will lose data.

See Also

Recipe 9.1 covers issuing a cancellation request.

9.8. Injecting Cancellation Requests

Problem

You have a layer of your code that needs to respond to cancellation requests and also issue its own cancellation requests to the next layer.

Solution

The .NET 4.0 cancellation system has built-in support for this scenario, known as linked cancellation tokens. A cancellation token source can be created linked to one (or many) existing tokens. When you create a linked cancellation token source, the resulting token is canceled when any of the existing tokens is canceled or when the linked source is explicitly canceled.

The following code performs an asynchronous HTTP request. The token passed into this method represents cancellation requested by the end user, and this method also applies a timeout to the request:

async Task<HttpResponseMessage> GetWithTimeoutAsync(string url,

CancellationToken cancellationToken)

{

var client = new HttpClient();

using (var cts = CancellationTokenSource

.CreateLinkedTokenSource(cancellationToken))

{

cts.CancelAfter(TimeSpan.FromSeconds(2));

var combinedToken = cts.Token;

return await client.GetAsync(url, combinedToken);

}

}

The resulting combinedToken is canceled when either the user cancels the existing cancellationToken or when the linked source is canceled by CancelAfter.

Discussion

Although our example only used a single CancellationToken source, the CreateLinkedTokenSource method can take any number of cancellation tokens as parameters. This allows you to create a single combined token from which you can implement your logical cancellation. For example, ASP.NET provides one token that represents the request timing out (HttpRequest.TimedOutToken) and another token that represents the user disconnecting (HttpResponse.ClientDisconnectedToken); handler code may create a linked token that responds to either of these cancellation requests.

One thing to keep in mind is the lifetime of the linked cancellation token source. Our previous example is the usual use case, where one or more cancellation tokens are passed into our method, which then links them together and passes them on as a combined token. Note that our example code is using the using statement, which ensures that the linked cancellation token source is disposed of when the operation is complete (and the combined token is no longer being used). Consider what would happen if we did not dispose of the linked cancellation token source: it is possible that this method may be called multiple times with the same (long-lived) existing token, in which case we would link a new token source each time the method is invoked. Even after the HTTP requests complete (and nothing is using the combined token), that linked source is still attached to the existing token. To prevent memory leaks like this, dispose of the linked cancellation token source when you no longer need the combined token.

See Also

Recipe 9.1 covers issuing cancellation requests in general.

Recipe 9.3 covers using cancellation as a timeout.

9.9. Interop with Other Cancellation Systems

Problem

You have some external or legacy code with its own notion of cancellation, and you want to control it using a standard CancellationToken.

Solution

The CancellationToken has two primary ways to respond to a cancellation request: polling (which we covered in Recipe 9.2) and callbacks (the subject of this recipe). Polling is normally used for CPU-bound code, such as data processing loops; callbacks are normally used in all other scenarios. You can register a callback for a token using the CancellationToken.Register method.

For example, let’s say we’re wrapping the System.Net.NetworkInformation.Ping type and we want to be able to cancel a ping. The Ping class already has a Task-based API but does not support CancellationToken. Instead, the Ping type has its own SendAsyncCancel method that we can use to cancel a ping. So, we register a callback that invokes that method, as follows:

async Task<PingReply> PingAsync(string hostNameOrAddress,

CancellationToken cancellationToken)

{

var ping = new Ping();

using (cancellationToken.Register(() => ping.SendAsyncCancel()))

{

return await ping.SendPingAsync(hostNameOrAddress);

}

}

Now, when a cancellation is requested, it will invoke the SendAsyncCancel method for us, canceling the SendPingAsync method.

Discussion

The CancellationToken.Register method can be used to interoperate with any kind of alternative cancellation system. However, do bear in mind that when a method takes a CancellationToken, a cancellation request should only cancel that one operation. Some alternative cancellation systems implement a cancel by closing some resource, which can cancel multiple operations; this kind of cancellation system does not map well to a CancellationToken. If you do decide to wrap that kind of cancellation in a CancellationToken, you should document its unusual cancellation semantics.

Keep in mind the lifetime of the callback registration. The Register method returns a disposable that should be disposed of when that callback is no longer needed. The preceding example code uses a using statement to clean up when the asynchronous operation completes. If we did not have that using statement, then each time we call that example code with the same (long-lived) CancellationToken, it would add another callback (which in turn keeps the Ping object alive). To avoid memory and resource leaks, dispose of the callback registration when you no longer need the callback.

See Also

Recipe 9.2 covers responding to a cancellation token by polling rather than callbacks.

Recipe 9.1 covers issuing cancellation requests in general.