【问题标题】:Updating progressbar in parallel loop在并行循环中更新进度条
【发布时间】:2020-05-05 08:01:40
【问题描述】:

我有一个 WinForm 应用程序,我正在尝试在并行循环中更新进度条。这是我的代码的 sn-p:

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 线程中的消息似乎没有按顺序处理,或者代码有问题。

任何想法可能是什么问题?

提前致谢。

[更新]

【问题讨论】:

  • 我强烈建议将进度条更新调用移到 lock 语句之外。目前,您在长期进度条更新期间阻止计数器增加(UI 线程切换!)。锁定区域应尽可能短。根据您的DoWork 方法需要多长时间,更新进度条的UI 线程切换可能需要更长时间。有时最好在例如轮询计数器。使用计时器间隔 300 毫秒,然后更新进度条。你也可以看看快速Interlocked.Increment方法。
  • @KBO 感谢您的建议,但它与这种行为有什么关系吗?我假设通过将进度条更新调用移出锁定区域,竞争条件会大大增加。在通过锁定进行长期进度条更新期间的当前计数器增加中,似乎每次运行中的某个随机点来回设置进度。
  • 您还分配了两次bar.Maximum,然后进度条最大值小于该值。这也可能导致这种行为。您应该删除行 bar.Maximum = value;
  • @KBO "根据你的 DoWork 方法需要多长时间,UI 线程切换更新进度条可能需要更长的时间",这个更长的更新是否意味着可能会更早地观察到下一个进度动画更新上一个?
  • 在调用SetProgress之前尝试记录counter的值。它将帮助您查看进度条的更新是否不按 FIFO 顺序,或者其他一些奇怪的东西。

标签: c# .net progress-bar task-parallel-library


【解决方案1】:

问题与Parallel.ForEach 的工作方式有关。您可能认为它只使用后台线程来完成工作,但实际上它也使用当前线程。也就是说,在Parallel.ForEach的执行过程中,当前线程扮演了一个工作线程的角色。在您的情况下,当前线程是 UI 线程。对于操作中涉及的后台线程,条件 if (progressBar1.InvokeRequired) 的计算结果为 true,对于 UI 线程,条件为 false

后台线程在您的示例中调用progressBar1.Invoke 方法。与BeginInvoke 不同,Invoke 是一个阻塞方法,并且只有在 UI 线程处理完提供的委托后才会返回。由于 UI 线程忙于处理自己的 files 集合分区,Invoke 将阻塞,因此所有后台线程都会卡住,唯一会继续前进的线程将是 UI 线程。最后,UI 线程将不得不等待其他线程交付他们最初收到的单个文件的结果以进行处理,这是他们无法做到的,因此Parallel.ForEach 将死锁。至少这是您发布的代码的预期结果。由于您没有观察到死锁,我的猜测是您的示例中缺少某些代码行(可能是对 Application.DoEvents 的调用?)可以解决死锁情况。

解决这种不愉快情况的最简单方法是阻止 UI 成为工作线程。只需使用Task.Run 方法,将整个并行处理卸载到ThreadPool 线程:

await Task.Run(() =>
{
    Parallel.ForEach(//...
});

您还必须使用async 关键字标记您的事件处理程序,否则编译器将不允许使用漂亮的await 运算符。

应用此修复后,您可能希望通过删除所有这些丑陋的 InvokeRequired/Invoke 并将其替换为现代的 Progress 对象来使您的代码更加优雅。如果从架构的角度来看,这也很容易将文件处理逻辑与 UI 相关的逻辑分开。如果您想了解如何使用Progress 类,可以阅读this 文章。

【讨论】:

  • 感谢您的解释,实际上我已经将整个并行工作卸载到后台工作人员,但问题仍然存在。
  • 嗯。在这种情况下,我无法重现该问题。我建议您使用您正在观察的问题的可重现示例来更新您的问题。
猜你喜欢
  • 1970-01-01
  • 2015-09-08
  • 1970-01-01
  • 1970-01-01
  • 2016-07-30
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多