【发布时间】:2019-06-27 08:14:28
【问题描述】:
我创建了一个 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");
}
}
}
当我按下click 按钮,然后快速按下cancel 按钮时,我只弹出一个“Caught”消息框,而不是两次。
假设主线程 id 是 1,工作线程是 2 和 3 所以我有两个问题:
Q1- 当我按下cancel 按钮时,工作线程 2 和 3 已经执行了 'parOpts.CancellationToken.ThrowIfCancellationRequested();' (当然我的鼠标点击不能像线程的执行一样快)。执行 ThrowIfCancellationRequested 时,cancelToken 并没有被取消,也就是说线程 2 和线程 3 的取消按钮都没有被点击,那这些工作线程怎么还会抛出异常呢?
Q2-为什么我只有一个弹出消息框,不应该是两个,一个用于线程2,一个用于线程3?
Q3- 我将 Parallel.ForEach 修改为:
try
{
Parallel.ForEach(files, parOpts, currentFile =>
{
Thread.Sleep(5000);
parOpts.CancellationToken.ThrowIfCancellationRequested();
});
}
catch (OperationCanceledException ex)
{
MessageBox.Show("Caught");
}
现在我可以在工作线程到达 ThrowIfCancellationRequested() 之前按下取消按钮,但我仍然只得到一个主线程抛出的异常。但是我按下了cancal按钮,token已经设置为取消,所以当辅助工作线程到达parOpts.CancellationToken.ThrowIfCancellationRequested();时,它不应该也抛出异常吗?并且这个异常不能被主线程中的try catch处理(每个线程都有自己的堆栈),所以我应该得到一个未处理的异常来停止应用程序,但事实并非如此,我只是得到一个由主线程抛出的异常,这个异常是主线程还是工作线程抛出的?
Q4-我修改代码为:
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");
}
}
又奇怪了,没有弹出messagebox,虽然token设置为cancel,但是MessageBox.Show(...)语句在parOpts.CancellationToken.ThrowIfCancellationRequested();的语句之上,所以MessageBox.Show()应该先执行,但是为什么呢它根本没有执行?还是 CLR 将 parOpts.CancellationToken.ThrowIfCancellationRequested(); 提升到顶部以隐式成为第一个语句?
Q5- 我将代码修改为:
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");
}
所以我有足够的时间按下取消按钮,有一个“捕获”消息,但为什么仍然有异常?现在我明白了 Parallel.ForEach 在所有资源昂贵的操作之前检查 CancellationToken.IsCancellationRequested`,这是否意味着 Parallel.ForEach 将在执行内部的所有语句后检查 IsCancellationRequested?我的意思是 Parallel.ForEach 会检查 IsCancellationRequested 两次,一次在第一条语句之前,一次在最后一条语句之后?
【问题讨论】:
-
评论不用于扩展讨论;这个对话是moved to chat。
-
好的,明确一点:数组包含两个应该模拟工作负载的字符串,对吧? primary 线程是您创建的使用
Task.Factory.StartNew(() => ProcessFiles());执行Parallel.ForEach循环的线程。Parallel.ForEach然后执行(在主线程上)并可能创建两个工作线程(一个用于每个负载或字符串)。 -
有两种情况。 第一种情况:您在操作委托中调用
ThrowIfCancellationRequested():当您使用try-catch 包围您的操作代码时,工作线程不会出现异常。如果没有这样的 try-catch,Parallel.ForEach会捕获异常。这将在主线程上。重新抛出此异常。但是由于您在工作线程上调用了ThrowIfCancellationRequested(),所以起源是工作线程。 -
第二种情况:您没有在您的操作中显式调用
CancellationToke.ThrowIfCancellationRequested(),或者在CancellationToke.ThrowIfCancellationRequested()方法之后发生取消,然后下次Parallel.ForEach检查CancellationToke.IsCancelRequested时,它会抛出异常。由于 `Parallel.ForEach 在主线程上执行,异常的来源将是主线程。 -
请再次检查我的答案。我对其进行了更新以解决您关于主线程和工作线程的实际线程数和定义的问题。向下滚动以获取有关如何以及在哪些线程上引发异常的摘要。
标签: c# .net wpf task-parallel-library