CancellationTokenSource 无法与 Parallel.ForEach 一起正常工作

CancellationTokenSource not working properly with Parallel.ForEach

我有以下代码:

 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);

我的问题是,如果 CancellationTokenSource 设置为在 10 秒内完成,为什么需要超过 20 秒来取消操作(并行)

此致

我假设你实际上不是

回应

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

发生这种情况是因为您没有取消 Parallel.ForEach

为了真正取消你需要使用

po.CancellationToken.ThrowIfCancellationRequested();

在 Parallel.ForEach 代码中

正如前面的回答所指出的,如果你想真正取消由 Task.Delay() 创建的任务,你需要使用 Task.Delay 的重载,它接受 CancellationToken

Task.Delay(i * 1000, po.CancellationToken).Wait();

public static Task Delay( TimeSpan delay, CancellationToken cancellationToken )

此处有更多详细信息

MSDN How to: Cancel a Parallel.For or ForEach Loop

没有好的Minimal, Complete, and Verifiable code example,就不可能完全理解你的场景。但是根据您发布的代码,您似乎希望 CancellationToken 影响 Parallel.ForEach().

的每个单独迭代的执行

然而,事实并非如此。 Parallel.ForEach() 方法同时调度各个操作,但是一旦这些操作开始,它们就不受 Parallel.ForEach() 方法的控制。如果你想让他们提前终止,你必须自己做。例如:

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

按照您现在的代码,在您取消令牌之前,所有 20 个操作几乎立即开始(有一个短暂的延迟,因为线程池为所有操作创建了足够的线程,如有必要)。也就是说,当您取消令牌时,Parallel.ForEach() 方法不再有办法避免启动操作;他们已经开始了!

既然你的个人行为不会打断自己,那么剩下的就是让他们全部完成。启动时间(包括等待线程池创建足够的工作线程),加上最长的总延迟(即开始一个动作的延迟加上该动作的延迟),决定了操作所花费的总时间,以及你的取消令牌没有效果。由于您最长的操作是 20 秒,因此 Parallel.ForEach() 操作的总延迟将始终至少为 20 秒。

通过进行我上面显示的更改,每个单独操作的延迟任务将在令牌到期时被您的令牌取消,从而导致任务取消异常。这也会导致操作本身提前终止。

请注意,将取消标记分配给 ParallelOptions.CancellationToken 属性 仍然有价值。即使取消发生得太晚无法阻止 Parallel.ForEach() 启动所有操作,通过在选项中提供标记,它可以识别每个操作抛出的异常是由选项中使用的相同取消标记引起的.这样,它就可以只抛出一个 OperationCanceledException,而不是将所有操作异常包装在 AggregateException.