Search code examples
c#task-parallel-libraryparallel.foreachcancellation-token

Do I need to check the CancellationToken in Parallel.ForEach?


Here is the documentation for How to: Cancel a Parallel.For or ForEach Loop

And I've written a method to use that feature:

static async Task SpreadCheer(CancellationToken cancellationToken)
{
    cancellationToken.ThrowIfCancellationRequested();
    var friends = await GetFriends();

    Parallel.ForEach(friends,
        new ParallelOptions
            { CancellationToken = cancellationToken},
        friend=>
        {
            SendCake(friend);
            // (*1)
        });
}

In the example in the documentation, on the last line of each loop, (*1). It adds a cancellationToken.ThrowIfCancellationRequested.

This confuses me, because then why did I pass it into the parallel options. I can also see that Parallel.ForEach will already throw after processing an item if the token is cancelled. It also throws immediately before starting any loops if the token is cancellation from the start. It is also my experience of running and testing this code that no explicit throw on cancelled is required.

Do I need to (should I even) place a cancellationToken.ThrowIfCancellationRequested in position (*1)?

Sure the documentation has an example of it, but it doesn't explicitly mention it, and if there was a unit test covering this, it's not code needed to pass that test. As far as I can tell.


Extra context:

Some context on cancelling. The reason for cancellation is that I have an Azure WebJob to SpreadCheer, Spreading Cheer can take a while, and I'd like to gracefully stop spreading cheer if Azure sends me a shutdown signal. I don't want to mess up anyone's cake.


Solution

  • No, there is no reason to place a cancellationToken.ThrowIfCancellationRequested() as the last line in the body delegate. Adding this line at this place serves no purpose. The Parallel.ForEach checks on each iteration a flag that is updated in the CancellationToken.Register(callback) delegate. This flag is updated practically at the same time with the flag stored inside the associated CancellationTokenSource itself (source code), so you won't make the cancellation more responsive by adding this line.

    That said, the ThrowIfCancellationRequested method is extremely lightweight. It just checks the value of an internal volatile field (source code). So although redundant, it won't have any noticeable effect on the performance of the parallel operation either.

    A note about PLINQ. The WithCancellation operator in PLINQ checks the supplied CancellationToken once every 64 iterations, so in this case a final ThrowIfCancellationRequested is not redundant. It makes the cancellation more responsive indeed.