C# Parallel.ForEach如何处理取消或ThrowIfCancellationRequested()和异常

C# Parallel.ForEach如何处理取消或ThrowIfCancellationRequested()和异常,c#,.net,wpf,task-parallel-library,C#,.net,Wpf,Task Parallel Library,我创建了一个WPF应用程序来查看TPL是如何工作的,我对我的输出感到困惑。下面是我的代码: // Two buttons, 'Process' button and 'Cancel' button public partial class MainWindow : Window { private CancellationTokenSource cancelToken = new CancellationTokenSource(); public MainWindow() {

我创建了一个WPF应用程序来查看TPL是如何工作的,我对我的输出感到困惑。下面是我的代码:

// Two buttons, 'Process' button and 'Cancel' button
public partial class MainWindow : Window 
{
   private CancellationTokenSource cancelToken = new CancellationTokenSource();
   public MainWindow()
   {
      InitializeComponent();
   }
   //...

   private void cmdProcess_Click(object sender, EventArgs e)  // Sequence A
   {
      Task.Factory.StartNew(() => ProcessFiles()); 
   }

    private void cmdCancel_Click(object sender, EventArgs e)   //Sequence B
   {
      cancelToken.Cancel();  
   }

   private void ProcessFiles() 
   {
      ParallelOptions parOpts = new ParallelOptions();
      parOpts.CancellationToken = cancelToken.Token;
      parOpts.MaxDegreeOfParallelism = System.Environment.ProcessorCount;

      string[] files = { "first", "second" };
      try
      {
         Parallel.ForEach(files, parOpts, currentFile =>
         {
            parOpts.CancellationToken.ThrowIfCancellationRequested();  //Sequence C
            Thread.Sleep(5000);
         });
      }
      catch (OperationCanceledException ex)
      { 
         MessageBox.Show("Caught");
      }
   }

}
当我按下
单击
按钮,然后快速按下
取消
按钮时,我只会弹出一次“捕获”消息框,而不是两次

假设主线程id为1,工作线程为2和3 所以我有两个问题:

Q1-当我按下取消按钮时,工作线程2和3已经执行了“parOpts.CancellationToken.throwifccancellationrequest();”(当然,我的鼠标点击不能像线程执行一样快)。当它们执行ThrowIfCancellationRequested时,cancelToken没有被取消,这意味着线程2和线程3没有单击cancel按钮。那么为什么那些工作线程仍然抛出异常呢

问题2-为什么我只得到一个弹出消息框,它不应该是两个,一个用于线程2,一个用于线程3吗

Q3-我将Parallel.ForEach修改为:

try
{
   Parallel.ForEach(files, parOpts, currentFile =>
   {
      Thread.Sleep(5000);
      parOpts.CancellationToken.ThrowIfCancellationRequested(); 

   });
}
catch (OperationCanceledException ex)
{ 
   MessageBox.Show("Caught");
}
现在我可以在工作线程到达ThrowIfCancellationRequested()之前按cancel按钮,但我仍然只得到主线程引发的一个异常。但是我按下了cancal按钮,令牌被设置为cancel,所以当辅助工作线程到达
parOpts.CancellationToken.throwifccancellationrequest()时,它不也应该抛出异常吗?这个异常不能由主线程中的try-catch处理(每个线程都有自己的堆栈),所以我应该得到一个未处理的异常来停止应用程序,但事实并非如此,我只得到一个由主线程引发的异常,这个异常是由主线程还是工作线程引发的

Q4-I将代码修改为:

private void ProcessFilesz()
{
    ParallelOptions parOpts = new ParallelOptions();
    parOpts.CancellationToken = cancelToken.Token;
    parOpts.MaxDegreeOfParallelism = System.Environment.ProcessorCount;

    cancelToken.Cancel(); // cancel here
    string[] files = { "first", "second" };
    try
    {
        Parallel.ForEach(files, parOpts, currentFile =>
        {
            MessageBox.Show("Underline Thread is " + Thread.CurrentThread.ManagedThreadId.ToString());
            parOpts.CancellationToken.ThrowIfCancellationRequested();
        });

    }
    catch (OperationCanceledException ex)
    {
        MessageBox.Show("catch");
    }
}
try
{
   Parallel.ForEach(files, parOpts, currentFile =>
   {
      Thread.Sleep(5000); // I pressed the cancel button on the main thread when the worker thread is sleeping
   });
}
catch (OperationCanceledException ex)
{
   MessageBox.Show("Caught");
}
还有一点很奇怪,没有messagebox弹出窗口,即使令牌被设置为cancel,但是
messagebox.Show(…)
语句在
parOpts.CancellationToken.throwifccancellationRequested()的语句之上
,因此应该首先执行
MessageBox.Show()
,但为什么不执行它呢?或CLR提升
parOpts.CancellationToken.throwifccancellationrequest()
到顶部隐式成为第一条语句

问题5-我将代码修改为:

private void ProcessFilesz()
{
    ParallelOptions parOpts = new ParallelOptions();
    parOpts.CancellationToken = cancelToken.Token;
    parOpts.MaxDegreeOfParallelism = System.Environment.ProcessorCount;

    cancelToken.Cancel(); // cancel here
    string[] files = { "first", "second" };
    try
    {
        Parallel.ForEach(files, parOpts, currentFile =>
        {
            MessageBox.Show("Underline Thread is " + Thread.CurrentThread.ManagedThreadId.ToString());
            parOpts.CancellationToken.ThrowIfCancellationRequested();
        });

    }
    catch (OperationCanceledException ex)
    {
        MessageBox.Show("catch");
    }
}
try
{
   Parallel.ForEach(files, parOpts, currentFile =>
   {
      Thread.Sleep(5000); // I pressed the cancel button on the main thread when the worker thread is sleeping
   });
}
catch (OperationCanceledException ex)
{
   MessageBox.Show("Caught");
}
所以我有足够的时间按下取消按钮,有一条“catch”消息,但为什么仍然有异常?现在我了解到Parallel.ForEach在所有耗费资源的操作之前检查CancellationToken.IsCancellationRequested`这是否意味着Parallel.ForEach将在执行所有内部语句之后检查IsCancellationRequested?我的意思是Parallel.ForEach将检查IsCancellationRequested两次,一次在第一个语句之前,一次在最后一个语句之后?

如何
并行。ForEach
处理取消 你的观察是正确的。但一切正常。由于设置了
ParallelOptions.CancellationToken
属性,因此
Parallel.ForEach
会将
OperationCanceledException
抛出一次
cancellationRequested

所有支持取消的框架类的行为都是这样的(例如,
Task.Run
)。在执行任何昂贵的资源分配(在内存或时间上昂贵)之前,为了提高效率,框架会在执行期间多次检查取消令牌。例如,
Parallel.ForEach
由于所有的线程管理,不得不进行许多这种昂贵的资源分配。在每个分配步骤(例如初始化、生成工作线程或分叉、应用分区器、调用操作等)之前,将再次计算
CancellationToken.IsCancelRequested

最后一个内部
Parallel.ForEach
步骤是在创建
ParallelLoopResult
(Parallel.ForEach
的返回值)之前连接线程。在此操作之前,将再次计算
CancellationToken.IsCancellationRequested
。由于在执行
Thread.Sleep(5000)
时取消了执行
Parallel.ForEach
,因此您必须等待最长5秒钟,直到框架重新检查此属性并可以抛出
操作取消异常。你可以测试一下。执行
线程需要x/1000秒。睡眠(x)
会一直持续到
消息框显示为止

另一个取消并行.ForEach
的机会委托给消费者。消费者的操作很可能是长时间运行的,因此需要在
Parallel.ForEach
结束之前取消。如您所知,可以通过(反复)调用
CancellationToken.ThrowIfCancellationRequested()
强制提前取消,这一次将使
CancellationToken
抛出
OperationCanceledException
(而不是
Parallel.ForEach

要回答上一个问题,为什么您只会看到一个
MessageBox
:在您已经注意到的特殊情况下,在代码到达
CancellationToken.ThrowIfCancellationRequested()
之前,您太慢了,无法单击取消按钮,但在线程从睡眠中醒来之前,您可以单击它。因此,
Parallel.ForEach
抛出异常(在连接线程和创建
ParallelLoopResult
之前)。因此抛出了一个异常。但是,即使在到达
CancellationToken.ThrowIfCancellationRequested()
之前足够快地取消了循环,仍然只有一个
MessageBox
显示,因为一旦抛出未修补的异常,循环就会中止所有线程。要允许每个线程抛出一个异常,您必须捕获每个线程并累积它们,然后再将它们封装在
AggregateException
中抛出。见:对于m
try 
{
  var task = new Task(() => DoParallel());
  task.Start();
  task.Wait();
}
catch (AggregateException ex)
{
  // Reachable code
}
try
{
   Parallel.ForEach(files, parOpts, currentFile =>
   {
      Thread.Sleep(5000);
      parOpts.CancellationToken.ThrowIfCancellationRequested(); 

   });
}
catch (OperationCanceledException ex)
{ 
   MessageBox.Show("Caught");
}
public void ThrowIfCancellationRequested()
{
  if (IsCancellationRequested) 
    ThrowOperationCanceledException();
}

// Throws an OCE; separated out to enable better inlining of ThrowIfCancellationRequested
private void ThrowOperationCanceledException()
{
  throw new OperationCanceledException(Environment.GetResourceString("OperationCanceled"), this);
}