【问题标题】:BackgroundWorker still freezes UIBackgroundWorker 仍然冻结 UI
【发布时间】:2014-03-05 17:35:33
【问题描述】:

我有以下代码, 正如您所看到的,后台工作人员搜索文件,并且在进行中更改的事件文件被添加到列表视图中,但是由于有很多文件被添加到列表视图中,UI 变得无响应,我可以在循环中休眠线程但是我认为这不是一个好习惯,防止 UI 冻结的最佳方法是什么?

更详细地说,listview 是表单上的表单控件。

void bg_DoWork(object sender, DoWorkEventArgs e)
{
    Stack<string> dirs = new Stack<string>(20);
    dirs.Push(e.Argument.ToString());
    while (dirs.Count > 0)
    {
        string currentDir = dirs.Pop();
        string[] subDirs;
        try { subDirs = System.IO.Directory.GetDirectories(currentDir); }
        catch (UnauthorizedAccessException) { continue; }
        catch (System.IO.DirectoryNotFoundException) { continue; }

        string[] files = null;
        try { files = System.IO.Directory.GetFiles(currentDir); }

        catch (UnauthorizedAccessException) { continue; }
        catch (System.IO.DirectoryNotFoundException) { continue; }
        foreach (var file in files) { bg.ReportProgress(0, file); }
        foreach (string str in subDirs) { dirs.Push(str); }
    }
}
    void bg_ProgressChanged(object sender, ProgressChangedEventArgs e)
    {
        listView1.Items.Add(e.UserState.ToString());
    }

【问题讨论】:

  • 你能多显示一点上下文吗?你怎么称呼后台工作人员? listView1 是 Windows 窗体控件吗?
  • 如果你建立一个完整的列表,然后在_WorkComplete 事件中将它添加到你的listView 中,它是如何执行的?还是操作太慢而不合适?
  • @IainBallard 是的 listview 是一个控件。
  • @patchandthat 我可能会这样做,还没有尝试过,但是在列表视图中添加一个巨大的列表有什么区别?我的意思是,这可能仍然占用 UI。
  • 如果目录中有很多文件,使用进度(0,文件)更新 UI 会使 UI 不得不做大量工作......

标签: c# user-interface backgroundworker


【解决方案1】:

所以这里的问题是ReportProgress 实际上是异步的。它不会等待相应的 UI 更新实际完成,然后继续工作。通常这很棒。在大多数情况下,没有任何令人信服的理由会为了等待 UI 更新而减慢您的高效工作。

不过有一个例外。如果您经常调用ReportProgress,以至于在添加下一个进度更新之前实际上没有时间完成上一个进度更新,那么最终发生的事情是您在消息队列中填满了更新进度的请求。你有这么多文件,获取这些文件列表只需要很少的时间。实际上,它比编组到 UI 线程和更新 UI 所需的时间要少得多。

因为这个队列最终会被备份,所以任何其他 UI 更新都需要等待这个长队列才能执行任何操作。

Batching up the updates and indicating progress less often is one possible solution。考虑到您的情况,它可能会或可能不会被接受。这几乎肯定会有所帮助,但取决于根据您正在执行的操作更新 UI 需要多长时间,以及生成数据的速度,可能会引起问题。如果它适用于您的具体情况,那就太好了。

另一个选项是更改您更新进度的方式,以便您的工作人员在继续之前等待 UI 更新。显然,除非您需要这样做,否则您应该避免这样做,因为这意味着虽然您在工作时不会冻结 UI,但您的工作将花费相当长的时间。虽然有很多方法可以做到这一点,其中最简单的可能只是使用Invoke不是BeginInvoke):

foreach (var file in files)
    listView1.Invoke(new Action(()=>listView1.Items.Add(file));

虽然从BackgroundWorker 调用Invoke 通常是代码异味,应该避免,但这是一种例外情况。

请注意,即使您确实 最终在此处使用Invoke,我仍然建议对调用进行批处理,以便每次调用添加的项目不止一项。如果单个目录中的文件数量足够少,请将整个 foreach 放在 Invoke 中,如果您的子目录往往只有很少的文件(即,它们非常深,而不是宽泛),请考虑将将所有文件放到一个临时列表中,直到它足够大到值得批处理到 Invoke 中。根据您的数据尝试不同的方法,看看哪种方法最有效。

【讨论】:

  • 关于 UI 消息队列由于其他线程试图异步更新它而得到备份的要点。我来到这里是因为我遇到了同样的问题并且没有想到消息队列问题。我的解决方案是使用私有 DateTime 变量来跟踪 UI 上次更新的时间,然后,在我的 ProgressChanged 事件中,仅在 (DateTime.Now - MyVariable).TotalMilliseconds 大于 10 时更新 UI - 或 - 如果当前迭代恰好是最后一个迭代。然后我只是将变量设置为 DateTime.Now,以便下次触发事件。效果很好。谢谢!
【解决方案2】:

bg.ReportProgress() 旨在将 BackgroundWorker 的整体进度报告回 UI 线程,以便您可以通知用户进度。但是,您使用它来实际将字符串添加到 ListView。您最好将文件列表编译到内存列表中,然后在后台工作人员完成时填充一次 listView1:

public void bg_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
   foreach (var file in MyFileListVar){
       listView1.Items.Add(file);
   }
}

【讨论】:

  • 这里的问题是,使用所有这些项目更新列表视图最终会花费很长时间,以至于 UI 将冻结相当长的一段时间。 OP 的代码会发生这种情况,这让我相信它很可能也会发生在这段代码中。
  • @Servy 是对的,试过了,没有任何区别,完全没有 UI 响应,就像我正在使用的代码一样。
  • 如果有太多的项目必须添加到列表中,以至于您的 UI 正在停止,那么您可能需要找到一种更好的方法来批量检索文件列表分页机制。我很好奇您的应用程序在尝试一次呈现如此大的项目列表时消耗了多少内存。
【解决方案3】:

尝试加载多个文件(假设在 10~50 之间),然后将它们发送回 UI 线程(即:bg.ReportProgress),而不是单独发送每个文件。

【讨论】:

    【解决方案4】:

    您不仅应该使用 RunWorkerCompleted 事件处理程序将项目添加到 ListView,还应该调用一次 AddRange 而不是多次添加。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-09-28
      • 1970-01-01
      • 1970-01-01
      • 2010-11-02
      • 1970-01-01
      相关资源
      最近更新 更多