简体   繁体   English

BackgroundWorker仍然冻结UI

[英]BackgroundWorker still freezes UI

I've got the following code, As you can see the background worker searches for files and in the progress changed event files are added to a listview, however since there is lots of files being added to the listview, the UI becomes unresponsive, I could Sleep the thread in the loops but I don't think that's a good practice, what's the best way to prevent the UI freeze? 我有以下代码,如您所见,后台工作人员正在搜索文件,并且在进度更改中将事件文件添加到列表视图中,但是由于有很多文件添加到列表视图中,因此UI变得无响应,可以让线程在循环中休眠,但是我认为这不是一个好习惯,防止UI冻结的最佳方法是什么?

to elaborate more, listview is a form control on a form. 详细说明,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());
    }

So the issue here is that ReportProgress is actually asynchronous. 因此,这里的问题是ReportProgress实际上是异步的。 It doesn't wait for the corresponding UI updates to actually be made before it continue moving on doing work. 在继续进行工作之前,它不会等待相应的UI更新真正完成。 Normally this is great. 通常这很好。 In most situations there's no compelling reason to slow down your productive work just to go wait for UI updates. 在大多数情况下,没有令人信服的理由只是为了等待UI更新而降低生产效率。

There's one exception though. 但是有一个例外。 If you call ReportProgress so often that it doesn't actually have time to complete the previous progress update before the next one is added, what ends up happening is that you fill up the message queue with requests to go update progress. 如果您如此频繁地调用ReportProgress以至于在添加下一个进度更新之前实际上没有时间来完成上一个进度更新,那么最终的结果是您在消息队列中填充了进行更新进度的请求。 You have so many files, and getting those lists of files takes very little time. 您有很多文件,并且获取这些文件列表只需要很少的时间。 It actually takes quite a bit less time than it takes to marshal to the UI thread and update the UI. 实际上,与编组UI线程和更新UI相比,它花费的时间要少得多。

Because this queue ends up being backed up, any other UI updates need to sit through that long queue before they can do anything. 由于最终会备份该队列,因此任何其他UI更新都需要经过较长的队列才能执行任何操作。

Batching up the updates and indicating progress less often is one possible solution . 一种可能的解决方案是分批更新并较少地指示进度 It may or may not be acceptable, given your situation. 鉴于您的情况,它可能会接受也可能不会接受。 It will almost certainly help, but depending on just how long it takes the UI to be updated based on whatever it is that you're doing, and how quickly you can generate data, it's possible that even this will cause problems. 几乎可以肯定会有所帮助,但是取决于您根据正在执行的操作更新UI所需的时间以及生成数据的速度,甚至可能会导致问题。 If it works for your specific case though, great. 如果它适合您的特定情况,那就太好了。

The other option is to change how you update progress such that your worker waits for the UI update before continuing. 另一个选项是更改更新进度的方式,以使工作人员在继续之前等待UI更新。 Obviously this is something that you should avoid unless you need to do it, because it means that while you aren't freezing the UI while you do your work, your work will take a fair bit longer. 显然,除非要这样做,否则您应该避免这种情况,因为这意味着虽然您在工作时不冻结UI,但是您的工作将花费更长的时间。 While there are any number of ways of doing this, the simplest of which is likely to just use Invoke ( not BeginInvoke ): 尽管有许多方法可以做到这一点,但最简单的方法可能只是使用Invoke而不是 BeginInvoke ):

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

While calling Invoke from a BackgroundWorker is generally code smell, and should be avoided, this is a somewhat exceptional case. 虽然从BackgroundWorker调用Invoke通常是代码异味,应该避免,但这是一种例外情况。

Note that even if you do end up resorting to using Invoke here I would still suggest batching the invokes such that you add more than just one item per invoke. 请注意,即使最终诉诸使用Invoke这里我还是建议拌和所调用,这样你添加超过每调用只有一个项目的更多。 If the number of files in a single directory is sufficiently low, put the whole foreach inside the Invoke , and if your subdirectories tend to have very few files (ie, they're very deep, not broad) consider even putting all of the files into a temp list until it's large enough to be worth batching into an Invoke . 如果单个目录中的文件数量足够少,则将整个foreach放入Invoke ,并且如果您的子目录往往只有很少的文件(即,它们很深,不是很宽),请考虑甚至将所有文件放入进入临时列表,直到它足够大以至于值得将其批量添加到Invoke Play around with different approaches based on your data, to see what works best. 根据您的数据尝试不同的方法,以了解最有效的方法。

bg.ReportProgress() is meant to report the overall progress of the BackgroundWorker back to the UI thread, so you can inform your users as to the progress. bg.ReportProgress()旨在将BackgroundWorker的总体进度报告回UI线程,因此您可以将进度告知用户。 However, you're using it to actually add strings to a ListView. 但是,您正在使用它实际将字符串添加到ListView。 You're better off compiling the list of files into an in-memory list and then populating listView1 once when the background worker completes: 最好将文件列表编译为内存列表,然后在后台工作程序完成时填充一次listView1:

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

尝试加载多个文件(比如说10到50之间),然后将它们发送回UI线程(即bg.ReportProgress ),而不是分别发送每个文件。

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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM