以并行方式将项目添加到 ListBox

Adding items to a ListBox in a parallel way

我正在编写一个简单的应用程序(用于测试目的),它将 10 M 元素添加到 ListBox。我正在使用 BackgroundWorker 来完成工作,并使用 ProgressBar 控件来显示进度。

每个元素只是一个“Hello World!”字符串和我在此过程中添加的索引。我的程序需要大约 7-8 秒来填充 ListBox,我想是否可以通过使用我 PC 上的所有可用内核 (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 秒 来填满我的列表框。 (元素也是无序的)

在这种情况下可以做什么,并行性是否会加快 "filling" 进程?

您在每次添加时都锁定了列表,而所有的进程负载就是向列表中添加一个元素,因此您没有加快速度而是减慢了它们的速度,因为实际上没有并行工作。

如果您的项目列表的大小已知(看起来),则创建一个具有适当大小的数组而不是列表,然后在并行 for 循环中将适当的项目设置为它的值,以这种方式不执行锁定,应该会更快。

此外,在您的代码中,您不会显示何时填充列表视图,只显示列表,所以我想您正在使用此列表作为数据源,在设置它之前执行 listView.BeginUpdate() 和设置listView.EndUpdate()后,可能会加快速度,m添加元素时listview有点慢。

如果您使用 Parallel.For,则不需要 BackgroundWorker。并且 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;
}

线程中的 lock 语句基本上将并行处理减少为顺序处理,但会产生获取锁的开销(使其实际上变慢)。

此外,这里可以使用的线程池线程数量有限,因此您不会同时添加全部 10m。

我认为更好的方法是使用非 UI 线程来填充列表然后绑定它 - 这将确保 UI 不是 frozen/unusable 而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。