So, after running all of these tests and observing the different behaviors, I think I can provide a summary on what to expect when cancelling tasks and parallel operations in this way. I also think it is important to understand this since cancellation behavior should be as consistent and predictable as possible. So here are my observed conclusions:
In every case, canceling a Task that has already completed has no effect on anything and attempting to cancel a Task that has not yet started will throw an InvalidOperationException.
When you run your own Task over an Action and you have passed a CancellationToken to both the Task and the Action, you have these scenarios:
- If the Action has started but is not yet in a status of “Running”, canceling the CancelationTokenSource will automatically fire the TaskCanceledException and terminate the Task, leaving it in a final status of “Canceled”.
- However, if the Action has started and is in a status of “Running”, the Action could respond to the IsCancellationRequested property of the CancellationToken and complete its work gracefully. In this case, the Task will also complete and report a final status of “RanToCompletion”. The cancel is “swallowed” by the Action, in this case and the Task is none the wiser.
- Alternatively, but still assuming that the Action has started and is in a status of “Running”, the Action could call the token.ThrowIfCancellationRequested() method which will terminate the Action immediately as well as terminate the Task. This leaves the Task with a final status of “Canceled”.
When using Parallel.For, Parallel.ForEach, and Parallel LINQ, the behavior is a little different. In these cases, the Cancel() operation is used for preventing the Parallel object from starting new operations, but leaves the operations that have already started alone. The operations, themselves, are unaware of the cancellation and have no reference to it.
If you wanted to set up an advanced situation, such as passing CancellationToken objects to Actions within a Parallel operation, be aware that the Framework communicates cancellation through Exceptions. The TaskCanceledException is fired for tasks that follow the patterns described in previous posts and the OperationCanceledException is fired for Parallel Operations. If you were passing CancellationToken objects to Actions within a Parallel operation, it might be wise to allow those Actions to handle the cancellation gracefully (i.e. don’t fire token.ThrowIfCancellationRequested within them) so that the general pattern of the Parallel operation is unaffected. It will automatically stop new operations from running, but will allow your already-running operations to run to a state of completion (which may include your graceful exit based on the token). I don’t know how often this type of situation would be necessary, but I’m sure someone is using it or trying it somewhere.
Also, there are additional constructs for controlling Parallel loops using a ParallelLoopState for breaking and stopping loops in specific ways, the discussion of these is outside my scope of CancellationTokenSource. See this msdn site as a starting point: Parallel.For Documentation