Ayorus Ayorus - 1 month ago 8
C# Question

CancellationTokenSource not working properly with Parallel.ForEach

I have the following code:

CancellationTokenSource ts = new CancellationTokenSource(10000);
ParallelOptions po = new ParallelOptions();
po.CancellationToken = ts.Token;

List<int> lItems = new List<int>();

for (int i = 0; i < 20; i++)
lItems.Add(i);

System.Collections.Concurrent.ConcurrentBag<int> lBgs = new System.Collections.Concurrent.ConcurrentBag<int>();

Stopwatch sp = Stopwatch.StartNew();
try
{
Parallel.ForEach(lItems, po, i =>
{
Task.Delay(i * 1000).Wait();
lBgs.Add(i);
});
}
catch (Exception ex)
{
}

Console.WriteLine("Elapsed time: {0:N2} seg Total items: {1}", sp.ElapsedMilliseconds / 1000.0, lBgs.Count);


My question is why takes more than 20 sec to cancel the operation (parallel for) if the CancelationTokenSource is set to finish in 10 sec

Regards

Answer

Without a good Minimal, Complete, and Verifiable code example, it's impossible to fully understand your scenario. But based on the code you posted, it appears that you expect for your CancellationToken to affect the execution of each individual iteration of the Parallel.ForEach().

However, that's not how it works. The Parallel.ForEach() method schedules individual operations concurrently, but once those operations start, they are out of the control of the Parallel.ForEach() method. If you want them to terminate early, you have to do that yourself. E.g.:

 Parallel.ForEach(lItems, po, i =>
 {
     Task.Delay(i * 1000, ts.Token).Wait();
     lBgs.Add(i);
  });

As your code stands now, all 20 actions are started almost immediately (there's a short delay as the thread pool creates enough threads for all the actions, if necessary), before you cancel the token. That is, by the time you cancel the token, the Parallel.ForEach() method no longer has a way to avoid starting the actions; they are already started!

Since your individual actions don't do anything to interrupt themselves, then all that's left is for them all to complete. The start-up time (including waiting for the thread pool to create enough worker threads), plus the longest total delay (i.e. the delay to start an action plus that action's delay), determines the total time the operation takes, with your cancellation token having no effect. Since your longest action is 20 seconds, the total delay for the Parallel.ForEach() operation will always be at least 20 seconds.

By making the change I show above, the delay task for each individual action will be cancelled by your token when it expires, causing a task-cancelled exception. This will cause the action itself to terminate early as well.

Note that there is still value in assigning the cancellation token to the ParallelOptions.CancellationToken property. Even though the cancellation happens too late to stop Parallel.ForEach() from starting all of the actions, by providing the token in the options, it can recognize that the exception thrown by each action was caused by the same cancellation token used in the options. With that, it then can throw just a single OperationCanceledException, instead of wrapping all of the action exceptions in an AggregateException.