我有一个WinForm应用程序,我正在尝试在并行循环中更新进度条。这是我的代码段:
Parallel.ForEach(files, (file, state) =>
{
//Intialization of parameters
//do cpu-intensive task
DoWork();
UpdateProgress();
});
int counter = 0;
private object updateLock = new object();
void UpdateProgress()
{
lock (updateLock)
{
counter++;
if (progressBar1.InvokeRequired)
{
progressBar1.Invoke(() => { progressBar1.SetProgress(counter); });
}
else
{
progressBar1.SetProgress(counter);
}
}
}
要获得进度条动画的即时更新,我使用SetProgress
。
public static void SetProgress(this ProgressBar bar, int value)
{
if (value == bar.Maximum)
{
bar.Maximum = value + 1;
bar.Value = value + 1;
bar.Maximum = value;
}
else
{
bar.Value = value + 1;
}
bar.Value = value;
}
整个过程似乎运行良好,但是进度条的更新方式存在问题。我随机看到进度动画是来回设置的,例如转到33/150,然后转到31/150,然后转到32/150。尽管我使用同步锁定对象相应地更新了每个步骤的进度,但似乎主UI线程中的消息未按顺序处理,或者代码有问题。
任何想法可能是什么问题?
提前感谢。
问题与Parallel.ForEach
的工作方式有关。您可能认为它只使用后台线程来完成工作,但实际上它也使用当前线程。换句话说,在执行Parallel.ForEach
期间,当前线程扮演工作线程的角色。在您的情况下,当前线程是UI线程。对于操作中涉及的后台线程,条件Parallel.ForEach
的值为if (progressBar1.InvokeRequired)
,对于UI线程,条件的值为true
。
在您的示例中,后台线程正在调用false
方法。与progressBar1.Invoke
不同,BeginInvoke
是一种阻塞方法,仅在UI线程处理了提供的委托后才返回。由于UI线程正忙于处理其自己的BeginInvoke
集合分区,因此Invoke
将被阻塞,因此所有后台线程将被卡住,并且唯一将继续取得进展的线程将是UI线程。最后,UI线程将不得不等待其他线程传递最初收到的用于处理的单个文件的结果,而这将无法执行,因此Invoke
将死锁。至少这是您发布的代码的预期结果。由于您没有观察到死锁,所以我的猜测是示例中缺少一些代码行(可能是对files
的调用?),可以解决死锁的情况。
解决此令人不快的情况的最简单方法是通过防止UI成为辅助线程。只需使用Invoke
方法,即可将整个并行处理卸载到Parallel.ForEach
线程中:
Application.DoEvents
您还必须用
Task.Run
关键字标记事件处理程序,否则编译器将不允许使用漂亮的Task.Run
运算符。
应用此修复程序后,您可能希望通过删除所有这些难看的ThreadPool
/ await Task.Run(() =>
{
Parallel.ForEach(//...
});
东西,并用一个现代的async
对象替换它,使代码更加优雅。如果从架构的角度来看,将文件处理逻辑与与UI相关的逻辑分开,这也将非常容易。如果您想学习如何使用await
类,则可以阅读await
文章。