BlackWaspTM

This web site uses cookies. By using the site you accept the cookie policy.This message is for compliance with the UK ICO law.

Parallel and Asynchronous
.NET 4.0+

Cancelling Parallel LINQ Queries

Parallel Language-Integrated Query (PLINQ) provides the performance benefits associated with parallel tasks, with the declarative approach of LINQ. This powerful combination also supports cancellation of queries, using standard cancellation tokens.

Parallel LINQ

Language-Integrated Query (LINQ) allows you to execute queries against many different types of sequence using a declarative, rather than imperative, approach. This means that the code expresses your intent, rather than specifying exactly how the operation is carried out. For example, to calculate a total value for a collection of orders, you might use the command shown below. The imperative version of this code could involve a foreach loop, a temporary variable and the code that performs the addition for each iteration.

decimal total = orders.Sum(o => o.OrderValue);

Parallel LINQ (PLINQ), introduced in the .NET framework version 4.0, adds parallel programming benefits to LINQ with only minor changes to the code. Adding the AsParallel extension method prepares a sequence for use with parallel versions of all of the LINQ standard query operators, each of which provides the possibility of parallel execution and the associated performance gains. To convert the previous example to its PLINQ equivalent, we simply add a call to AsParallel, as follows:

decimal total = orders.AsParallel.Sum(o => o.OrderValue);

PLINQ Cancellation

PLINQ queries are often executed in multi-threaded environments, in addition to the threads generated by their inherent parallelism. This is because PLINQ queries are generally used for long-running queries; fast-executing queries do not achieve the same performance benefits, as the overhead of generating and synchronising threads offsets the improvements provided by parallelism. When a query takes a long time to run, it is often worthwhile moving it to its own thread, allowing other threads to continue. For example, in software with a user interface (UI), a long query can be executed separately to the UI thread so that the program remains responsive to user input.

In such situations, it can be useful to allow a query to be cancelled before it completes. This cancellation is usually in response to an activity performed by the user, such as clicking a Cancel button. However, it is equally valid for a PLINQ query to be stopped prematurely without user intervention.

In this article we'll demonstrate the cancellation of PLINQ queries using a console application. We will execute two long-running queries and permit them to be terminated with a key press. To recreate the examples, open a new console application project and add the following using directives to simplify access to the namespaces that we need:

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

Cancellation Tokens

The Task Parallel Library and PLINQ share the same underlying mechanism for cancellation, this being the use of cancellation tokens. Tokens are generated from a CancellationTokenSource, and are provided to parallel loops, Task objects, calls to Parallel.Invoke, or PLINQ queries. When any of these processes needs to be cancelled, the CancellationTokenSource's Cancel method is executed. This changes the status of the cancellation tokens, allowing the tasks, loops and queries to react accordingly. Tokens can even be shared across a number of these parallel operations, allowing their co-ordinated cancellation.

In the case of parallel tasks, requesting cancellation does not guarantee that the operation is terminated. The tasks must check the token and exit gracefully. For loops, no individual iteration will be cancelled early, unless the code within the loop explicitly permits this. All scheduled iterations will be allowed to complete but unscheduled ones will not be started. This co-operative cancellation approach ensures that code is not terminated unexpectedly, leaving the software in an invalid state.

When you cancel a PLINQ query, this pattern is repeated. An individual operation within an iteration of the query will not be terminated unless you specifically include code for this purpose. Unscheduled iterations will not be started but ones that have already commenced will be completed before the query stops.

As with other parallel operations, when a PLINQ query is cancelled an OperationCanceledException is thrown. You should therefore catch this type of exception so that it does not remain unhandled. When there are other exceptions thrown within a query, these are wrapped in an AggregateException. This exception type should also be caught and its InnerExceptions property examined to find anything not related to cancellation.

To assist in processing aggregated exceptions we'll use the WithoutCancellations extension method, which I described in an earlier article:

public static class AggregateExceptionExtensions
{
    public static AggregateException WithoutCancellations(this AggregateException exception)
    {
        try { exception.Handle(e => { return e is OperationCanceledException; }); }
        catch (AggregateException ex) { return ex; }
        return new AggregateException();
    }
}
7 October 2012