Adding items to a ListBox in a parallel way

1.9k views Asked by At

I'm writing a simple application (for testing purposes) that adds 10 M elements to a ListBox. I'm using a BackgroundWorker to do the work and a ProgressBar control to display the progress.

My application

Each element is a just a "Hello World!" string with and index that I'm adding during the process. My program takes ~ 7-8 seconds to fill the ListBox, and I thought if it's possible to speed up this, by using all the available cores on my PC (8).

To achieve that, I've tried to use the TPL library, more precise the Parallel.For loop, but the results are unpredictable or it doesn't work as I want it to.

Here's the code of my application:

    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();
    }
}

And the parallel implementation that I've tried to write (this goes in 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;
            }
        });

The results is that the application freezes, and takes about 15 seconds to fill up my ListBox. (The elements are also unordered)

What can be done in this case and will parallelism speed up the "filling" process?

3

There are 3 answers

5
James Harcourt On BEST ANSWER

The lock statement in your thread basically reduces your parallel processing to sequential processing, but with the overhead of acquiring a lock (making it effectively slower).

Also there are a limited number of thread pool threads which can be used here so you won't get your full 10m concurrently adding.

I think a better way is to use a non UI thread to populate the list and then bind it afterwards - this will ensure the UI isn't frozen/unusable while the 10 million iteration loop is running:

public MainWindow()
{
    InitializeComponent();
    Task.Factory.StartNew(PopList);
}

Then you can call the UI thread when needed:

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; }));
}

In an MVVM world you can just set the bound IEnumerable instead of the ItemsSource as shown in the above example.

3
almulo On

If you use Parallel.For, then you don't need a BackgroundWorker. And the Worker doesn't work anymore as expected anyway, since you're trying to access it from another thread.

Remove the BackgroundWorker and do the Parallel.For directly, using Interlocked methods to update the progress bar:

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;
}
6
Gusman On

You are locking the list on each add, and all the process load is just that, adding an element to the list, so instead of speeding up things you are slowing them because there are really no parallel work.

If your list of items is of a known size (as it seems), instead of a list create an array with the appropiated size and then in the parallel for loop set the appropiated item to it's value, in this way no locking is performed and it should be faster.

Also, in your code you don't show when the list view is populated, just the list, so I suppose you are using this list as datasource, before setting it do a listView.BeginUpdate() and after setting it listView.EndUpdate(), it may speed up things a bit,m the listview is a bit slow when adding elements.