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

  • The reason why you should use token.ThrowIfCancellationRequested() within the operations itself is, that the parallel operations will continue to execute until the very end.

    Parallel.ForEach only checks the token at the beginning and at the end of the operation, not inbetween.

    So if you have a lot of parallel operations, or long running operations, you will want to check for cancellation within the operation yourself.

    You can verify the behavior with the following snippet, with or without the token.ThrowIfCancellationRequested()

    internal class Program
    {
        private static void Main()
        {
            var source = new CancellationTokenSource();
    
            var token = source.Token;
    
            Task.Run(() =>
            {
                Parallel.ForEach(Enumerable.Range(1, 100), new ParallelOptions {CancellationToken = token},
                    i =>
                    {
                        for (var y = 0; y < 100; y++)
                        {
                            token.ThrowIfCancellationRequested();
                            Thread.Sleep(1000);
                            Console.WriteLine($"{i} {y}");
                        }
                    });
            }, token);
    
            Console.WriteLine("press return to cancel...");
            Console.ReadLine();
    
            source.Cancel();
    
            Console.WriteLine("press return to exit...");
            Console.ReadLine();
        }
    }