以并行方式向ListBox添加项目

3
我正在编写一个简单的应用程序(用于测试目的),将10M个元素添加到ListBox中。我使用一个BackgroundWorker来执行工作,使用ProgressBar控件显示进度。
每个元素只是一个带有索引的"Hello World!"字符串,我在过程中添加。我的程序需要7-8秒才能填满ListBox,我想知道是否可以通过使用计算机上所有可用的核心(8个)来加快速度。
为了达到这个目的,我尝试使用TPL库,更精确地说是使用Parallel.For循环,但结果是不可预测的,或者它不能按照我希望的方式工作。
以下是我的应用程序代码:
    private BackgroundWorker worker = new BackgroundWorker();
    private Stopwatch sw = new Stopwatch();
    private List<String> numbersList = new List<String>();

    public MainWindow()
    {
        InitializeComponent();

        worker.WorkerReportsProgress = true;
        worker.DoWork += worker_DoWork;
        worker.ProgressChanged += worker_ProgressChanged;
        worker.RunWorkerCompleted += worker_RunWorkerCompleted;
    }

    private void btnAdd_Click(object sender, RoutedEventArgs e)
    {
        worker.RunWorkerAsync();
    }

    private void worker_DoWork(object sender, DoWorkEventArgs e)
    {
        sw.Start();

        int max = 10000000;
        int oldProgress = 0;

        for (int i = 1; i <= max; i++)
        {
            numbersList.Add("Hello World! [" + i + "]");

            int progressPercentage = Convert.ToInt32((double)i / max * 100);

            // Only report progress when it changes
            if (progressPercentage != oldProgress)
            {
                worker.ReportProgress(progressPercentage);
                oldProgress = progressPercentage;
            }
        }
    }

    private void worker_ProgressChanged(object sender, ProgressChangedEventArgs e)
    {
        pb.Value = e.ProgressPercentage;
    }

    private void worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
    {
        lstLoremIpsum.ItemsSource = numbersList;
        lblCompleted.Content = "OK";
        lblCompleted.Content += " (" + numbersList.Count + " elements added" + ")";
        lblElementiLista.Content += " (" +sw.Elapsed.TotalSeconds + ")";

        worker.Dispose();
    }
}

我尝试编写的并行实现(应放在DoWork中)如下:

        Parallel.For(1, max, i =>
        {
            lock (lockObject)
            {
                numbersList.Add("Hello World! [" + i + "]");
            }

            int progressPercentage = Convert.ToInt32((double)i / max * 100);

            // Only report progress when it changes
            if (progressPercentage != oldProgress)
            {
                worker.ReportProgress(progressPercentage);
                oldProgress = progressPercentage;
            }
        });

结果是应用程序会冻结,并需要15秒左右的时间才能填充ListBox。(元素也是无序的)
在这种情况下可以采取什么措施,使用并行处理能加速“填充”过程吗?
3个回答

3
你的线程中的lock语句基本上将并行处理减少为顺序处理,但却需要获取锁(使其变得更慢)。此外,这里可以使用的线程池线程数量有限,因此您无法同时获得完整的10m添加。
我认为更好的方法是使用非UI线程来填充列表,然后在之后进行绑定 - 这将确保UI在运行1000万次迭代循环时不会被冻结/无法使用。
public MainWindow()
{
    InitializeComponent();
    Task.Factory.StartNew(PopList);
}

需要时,您可以调用UI线程:

private void PopList()
{
    sw.Start();

    int max = 10000000;
    int oldProgress = 0;

    for (int i = 1; i <= max; i++)
    {
        numbersList.Add("Hello World! [" + i + "]");

        int progressPercentage = Convert.ToInt32((double)i / max * 100);

        // Only report progress when it changes
        if (progressPercentage != oldProgress)
        {
            Dispatcher.BeginInvoke(new Action(() => { pb.Value = progressPercentage; }));                    
            oldProgress = progressPercentage;
        }
    }

    Dispatcher.BeginInvoke(new Action(() => { lstLoremIpsum.ItemsSource = numbersList; }));
}

在MVVM架构中,您只需像上面的示例那样设置绑定的IEnumerable,而不是设置ItemsSource。

只需要4.5秒,很不错。我该如何使用你的示例来利用所有的CPU核心? - deefrson
如果您再次开始使用Parallel.For,仍然需要在List对象周围实现锁定,这将完全抵消并使并行处理变慢。这不是真正的CPU绑定任务-而且List.Add非常快,因此这里没有真正的CPU负载要分配。 - James Harcourt
所以没有办法进一步加快这个吗?如果它不是真正的与CPU相关的任务,那我应该看哪里呢?谢谢。 - deefrson
我认为你不可能在不影响其他方面的情况下加速它。你可以尝试将作业分割成10个并行线程,填充1-1000000、1000001-2000000等范围内的数据。但最后你仍需要连接这些列表,这会耗费时间。 - James Harcourt
我会研究一下那种方法,我也考虑过。再次感谢。由于我非常喜欢任务,所以我将您的答案标记为最佳! - deefrson

0
如果您使用Parallel.For,则不需要BackgroundWorker。而且由于您正在尝试从另一个线程访问Worker,因此该Worker也不会再按预期工作了。
删除BackgroundWorker并直接使用Parallel.For,在使用Interlocked方法更新进度条时进行操作:
private int ProgressPercentage { get; set; }

private void DoWork()
{
    Parallel.For(1, max, i =>
    {
        lock (lockObject)
        {
            numbersList.Add("Hello World! [" + i + "]");
        }

        int progressPercentage = Convert.ToInt32((double)i / max * 100);

        // Only report progress when it changes
        if (progressPercentage != oldProgress)
        {
            Interlocked.Exchange(ProgressPercentage, progressPercentage);
            ShowProgress();
        }
    });
}

private void ShowProgress()
{
    pb.Value = ProgressPercentage;
}

我已将其更改为Interlocked.Exchange(ref ProgressPercentage, progressPercentage),但VS正在抱怨“属性、索引器或动态成员访问可能不能作为out或ref参数传递”。 - deefrson
嗯...现在无法尝试,但如果您将ProgressPercentage更改为常规字段而不是属性会怎样呢?private int progressPercentage; - almulo
如果我将它更改为字段,错误就会消失,但是它会抛出另一个错误,说它无法在ShowProgress()函数的另一个线程上访问此对象。我想这是典型的跨线程异常。 - deefrson

0

你在每次添加时锁定列表,而所有的处理负载都只是添加一个元素到列表中,所以你并没有加速事情,反而减慢了它们,因为实际上没有并行工作。

如果你的项目列表大小已知(似乎是这样),那么不要使用列表,而是创建一个具有适当大小的数组,然后在并行循环中将适当的项设置为其值,这样就不会执行任何锁定操作,应该会更快。

此外,在你的代码中,你没有显示何时填充列表视图,只有列表,所以我想你正在使用这个列表作为数据源,在设置它之前,请使用listView.BeginUpdate(),在设置之后使用listView.EndUpdate(),这可能会稍微加快一些速度,因为当添加元素时,列表视图有点慢。


实际上,如果NumbersList属性被用作ListView的ItemsSource,我猜直接将每个项添加到ItemsSource中相比较而言,更快的方法是将不同的数组填充到项目中,然后将NumbersList属性设置为New List<String>(NumbersArray) - almulo
不,你错了,首先填充数组,一旦填充完成,然后将数组设置为项源,如果你已经可以使用数组,为什么要将其添加到列表中呢? - Gusman
好的,你可能更喜欢使用List而不是Array作为ItemsSource有很多原因...但无论如何,我只是在指出将集合设置为ItemsSource,然后逐个添加项并不是最优的方法:P 他现在似乎正在这样做。 - almulo
我怀疑,List<>不是一个可观察的集合,如果你只是向其中添加项目,ListView将不会被通知到这些更改,所以他没有这样做。 - Gusman
既然你提到了,他似乎也没有使用INotifyPropertyChanged。那么这些项是如何更新的呢? - almulo
正如我所说,他会在列表填充完成后将其设置为该项的属性。 - Gusman

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接