C# Parallel.ForEach如何处理取消或ThrowIfCancellationRequested()和异常
我创建了一个WPF应用程序来查看TPL是如何工作的,我对我的输出感到困惑。下面是我的代码: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() {
// 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);
}