-3

I have a WinForms C# program where I will have up to 1 million business objects open at once (in memory) on the user's machine.

My manager has asked for a real simple filter on these business objects. So if you filter on "Fred", the user will be shown a list of all objects which contains "Fred" in any of the text fields (Name, Address, Contact person etc). Also, this needs to be as close to real time as possible without blocking the UI. So, if you enter "Fred" into the filter text box, as soon as "F" is typed, the search will start looking for results with "F" in any text field (I am thinking that I may insist on a minimum of 3 characters in the search). When the text box is changed to "Fr", the old search will be stopped (if still executing) and a new search started.

This is a highly CPU intensive operation on the user's local machine with zero IO. This sounds like I should fire off separate tasks to run on separate threads on separate cores on my CPU. When they are all done combine the results back into one list and display result to the user.

I am old school, this sounds like a job for a BackgroundWorker, but I read that BackgroundWorker is explicitly labelled as obsolete in .NET 4.5 (sad face). See: Async/await vs BackgroundWorker

I find many posts that say I should replace BackgroundWorker with the new async await c# commands.

BUT, there are few good examples of this and I find comments along the lines of "async await does not guarantee separate threads" and all of the examples show IO / Network intensive tasks on the awaited task (not CPU intensive tasks).

I found a good example of BackgroundWorker that looked for prime numbers, which is a similar CPU intensive task and I played around with that and found that it would meet most of my needs. But I have the problem that BackgroundWorker is obsolete in .NET 4.5.

My findings from BackgroundWorker investigation are:

  • Best performance improvement is gained when you have one task per physical core on the machine, my VM has 3 cores, the task ran quickest with 3 Background Worker tasks.
  • Performance dies when you have too many Background Worker tasks.
  • Performance dies when you have too many progress notifications back to the UI thread.

Questions:

Is Background worker the right technique to use for a CPU intensive task like this? If not, what technique is better? Are there any good examples out there for a CPU intensive task like this? What risks am I taking if I use Background worker?

Code example based on a single Background Worker

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

// This code is based on code found at: https://social.msdn.microsoft.com/Forums/vstudio/en-US/b3650421-8761-49d1-996c-807b254e094a/c-backgroundworker-for-progress-dialog?forum=csharpgeneral
// Well actually at: http://answers.flyppdevportal.com/MVC/Post/Thread/e98186b1-8705-4840-ad39-39ac0bdd0a33?category=csharpgeneral

namespace PrimeNumbersWithBackgroundWorkerThread
{
  public partial class Form_SingleBackground_Worker : Form
  {
    private const int        _MaxValueToTest    = 300 * 1000; 
    private const int        _ProgressIncrement = 1024 * 2  ; // How often to display to the UI that we are still working
    private BackgroundWorker _Worker;
    private Stopwatch        _Stopwatch;
    public Form_SingleBackground_Worker()
    {
      InitializeComponent();
    }
    private void btn_Start_Click           ( object sender, EventArgs e)
    {
      if ( _Worker == null )
      {
        progressBar.Maximum                 = _MaxValueToTest;
        txt_Output.Text                     = "Started";
        _Stopwatch                          = Stopwatch.StartNew();
        _Worker                             = new BackgroundWorker();
        _Worker.WorkerReportsProgress       = true;
        _Worker.WorkerSupportsCancellation  = true;
        _Worker.DoWork                     += new DoWorkEventHandler            ( worker_DoWork             );
        _Worker.ProgressChanged            += new ProgressChangedEventHandler   ( worker_ProgressChanged    );
        _Worker.RunWorkerCompleted         += new RunWorkerCompletedEventHandler( worker_RunWorkerCompleted );
        _Worker.RunWorkerAsync( _MaxValueToTest );  // do the work
      }
    }
    private void btn_Cancel_Click          ( object sender, EventArgs e)
    {
      if ( _Worker != null && _Worker.IsBusy)
      {
        _Worker.CancelAsync();
      }
    }
    private void worker_DoWork             ( object sender, DoWorkEventArgs e)
    {
      int              lMaxValueToTest    = (int)e.Argument;
      BackgroundWorker lWorker            = (BackgroundWorker)sender; // BackgroundWorker running this code for Progress Updates and Cancelation checking
      List<int>        lResult            = new List<int>(); 
      long             lCounter           = 0;

      //Check all uneven numbers between 1 and whatever the user choose as upper limit
      for (int lTestValue = 1; lTestValue < lMaxValueToTest; lTestValue += 2)
      {
        lCounter++;
        if ( lCounter % _ProgressIncrement == 0 )
        {
          lWorker.ReportProgress(lTestValue);  // Report progress to the UI every lProgressIncrement tests (really slows down if you do it every time through the loop)
          Application.DoEvents();

          //Check if the Cancelation was requested during the last loop
          if (lWorker.CancellationPending )
          {
            e.Cancel = true; //Tell the Backgroundworker you are canceling and exit the for-loop
            e.Result = lResult.ToArray(); 
            return;
          }
        }

        bool lIsPrimeNumber = IsPrimeNumber( lTestValue ); //Determine if lTestValue is a Prime Number
        if ( lIsPrimeNumber )
          lResult.Add(lTestValue);
      }
      lWorker.ReportProgress(lMaxValueToTest);  // Tell the progress bar you are finished
      e.Result = lResult.ToArray();                // Save Return Value
    }
    private void worker_ProgressChanged    ( object sender, ProgressChangedEventArgs e)
    {
      int lNumber       = e.ProgressPercentage;
      txt_Output.Text   = $"{lNumber.ToString("#,##0")} ({(lNumber/_Stopwatch.ElapsedMilliseconds).ToString("#,##0")} thousand per second)";
      progressBar.Value = lNumber;
      Refresh();
    }
    private void worker_RunWorkerCompleted ( object sender, RunWorkerCompletedEventArgs e)
    {
      progressBar.Value = progressBar.Maximum;
      Refresh();

      if ( e.Cancelled )
      {
        txt_Output.Text = "Operation canceled by user";
        _Worker         = null;
        return;
      }
      if ( e.Error != null)
      {
        txt_Output.Text = $"Error: {e.Error.Message}";
        _Worker         = null;
        return;
      }
      int[]  lIntResult = (int[])e.Result;
      string lStrResult = string.Join( ", ", lIntResult );
      string lTimeMsg   = $"Calculate all primes up to {_MaxValueToTest.ToString("#,##0")} with \r\nSingle Background Worker with only 1 worker: Total duration (seconds): {_Stopwatch.ElapsedMilliseconds/1000}";
      txt_Output.Text   = $"{lTimeMsg}\r\n{lStrResult}";
      _Worker           = null;
    }
    private bool IsPrimeNumber             ( long aValue )
    {
      // see https://en.wikipedia.org/wiki/Prime_number
      // Among the numbers 1 to 6, the numbers 2, 3, and 5 are the prime numbers, while 1, 4, and 6 are not prime.
      if ( aValue <= 1 ) return false;
      if ( aValue == 2 ) return true ;
      if ( aValue == 3 ) return true ;
      if ( aValue == 4 ) return false;
      if ( aValue == 5 ) return true ;
      if ( aValue == 6 ) return false;
      bool      lIsPrimeNumber = true;
      long      lMaxTest       = aValue / 2 + 1;
      for (long lTest          = 3; lTest < lMaxTest && lIsPrimeNumber; lTest += 2)
      {
        long lMod = aValue % lTest;
        lIsPrimeNumber = lMod != 0;
      }
      return lIsPrimeNumber;
    }
  }
}

Code example based on multiple Background Workers

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

// This code is based on code found at: https://social.msdn.microsoft.com/Forums/vstudio/en-US/b3650421-8761-49d1-996c-807b254e094a/c-backgroundworker-for-progress-dialog?forum=csharpgeneral
// Well actually at: http://answers.flyppdevportal.com/MVC/Post/Thread/e98186b1-8705-4840-ad39-39ac0bdd0a33?category=csharpgeneral

namespace PrimeNumbersWithBackgroundWorkerThread
{
  public partial class Form_MultipleBackground_Workers : Form
  {
    private const int              _MaxValueToTest    = 300 * 1000; 
    private const int              _ProgressIncrement = 1024 * 2  ; // How often to display to the UI that we are still working
    private int                    _NumberOfChuncks   = 2         ; // Best performance looks to be when this value is same as the number of cores
    private List<BackgroundWorker> _Workers           = null      ;
    private List<WorkChunk>        _Results           = null      ;
    private Stopwatch              _Stopwatch;
    public Form_MultipleBackground_Workers () { InitializeComponent(); }
    private void btn_Start_Click           ( object sender, EventArgs e)
    {
      if ( _Workers == null )
      {
        progressBar.Maximum   = _MaxValueToTest;
        txt_Output.Text       = "Started";
        _Stopwatch            = Stopwatch.StartNew();
        _Workers              = new List<BackgroundWorker>();
        _Results              = new List<WorkChunk>();
        int lChunckSize       = _MaxValueToTest / _NumberOfChuncks;
        int lChunckStart      = 1;
        while ( lChunckStart <= _MaxValueToTest )
        {
          int lChunckEnd = lChunckStart + lChunckSize;
          if ( lChunckEnd > _MaxValueToTest ) lChunckEnd = _MaxValueToTest;
          BackgroundWorker lWorker = StartAWorker( lChunckStart, lChunckEnd );
          _Workers.Add( lWorker );
          lChunckStart += lChunckSize + 1;
        }
      }
    }
    private BackgroundWorker StartAWorker  ( int aRangeStart, int aRangeEnd )
    {
      WorkChunk        lWorkChunk         = new WorkChunk() { StartRange = aRangeStart, EndRange = aRangeEnd };
      BackgroundWorker lResult            = new BackgroundWorker();
      lResult.WorkerReportsProgress       = true;
      lResult.WorkerSupportsCancellation  = true;
      lResult.DoWork                     += new DoWorkEventHandler            ( worker_DoWork             );
      lResult.ProgressChanged            += new ProgressChangedEventHandler   ( worker_ProgressChanged    );
      lResult.RunWorkerCompleted         += new RunWorkerCompletedEventHandler( worker_RunWorkerCompleted );
      lResult.RunWorkerAsync( lWorkChunk );  // do the work
      Console.WriteLine( lWorkChunk.ToString() );
      return lResult;
    }
    private void btn_Cancel_Click          ( object sender, EventArgs e)
    {
      if ( _Workers != null )
      {
        foreach( BackgroundWorker lWorker in _Workers )
        {
          if ( lWorker.IsBusy )
            lWorker.CancelAsync();
        }
      }
    }
    private void worker_DoWork             ( object sender, DoWorkEventArgs e)
    {
      WorkChunk        lWorkChunk         = (WorkChunk)e.Argument;
      BackgroundWorker lWorker            = (BackgroundWorker)sender; // BackgroundWorker running this code for Progress Updates and Cancelation checking
      int              lCounter           = 0;
      e.Result = lWorkChunk; 
      lWorkChunk.StartTime = DateTime.Now;
      lWorkChunk.Results   = new List<int>();

      // Check all uneven numbers in range
      for ( int lTestValue = lWorkChunk.StartRange; lTestValue <= lWorkChunk.EndRange; lTestValue++ )
      {
        lCounter++;
        if ( lCounter % _ProgressIncrement == 0 )
        {
          lWorker.ReportProgress(lCounter);  // Report progress to the UI every lProgressIncrement tests (really slows down if you do it every time through the loop)
          Application.DoEvents();            // This is needed for cancel to work
          if (lWorker.CancellationPending )  // Check if Cancelation was requested
          {
            e.Cancel = true; //Tell the Backgroundworker you are canceling and exit the for-loop
            lWorkChunk.EndTime = DateTime.Now;
            return;
          }
        }

        bool lIsPrimeNumber = IsPrimeNumber( lTestValue ); //Determine if lTestValue is a Prime Number
        if ( lIsPrimeNumber )
          lWorkChunk.Results.Add(lTestValue);
      }
      lWorker.ReportProgress( lCounter );  // Tell the progress bar you are finished
      lWorkChunk.EndTime = DateTime.Now;
    }
    private void worker_ProgressChanged    ( object sender, ProgressChangedEventArgs e)
    {
      int lNumber       = e.ProgressPercentage;
      txt_Output.Text   = $"{lNumber.ToString("#,##0")} ({(lNumber/_Stopwatch.ElapsedMilliseconds).ToString("#,##0")} thousand per second)";
      progressBar.Value = lNumber;
      Refresh();
    }
    private void worker_RunWorkerCompleted ( object sender, RunWorkerCompletedEventArgs e)
    {
      // All threads have to complete before we have real completion
      progressBar.Value = progressBar.Maximum;
      Refresh();

      if ( e.Cancelled )
      {
        txt_Output.Text = "Operation canceled by user";
        _Workers        = null;
        return;
      }
      if ( e.Error != null)
      {
        txt_Output.Text = $"Error: {e.Error.Message}";
        _Workers        = null;
        return;
      }
      WorkChunk lPartResult = (WorkChunk)e.Result;
      Console.WriteLine( lPartResult.ToString() );
      _Results.Add( lPartResult );
      if ( _Results.Count == _NumberOfChuncks )
      {
        // All done, all threads are back
        _Results = (from X in _Results orderby X.StartRange select X).ToList(); // Make sure they are all in the right sequence
        List<int> lFullResults = new List<int>();
        foreach ( WorkChunk lChunck in _Results )
        {
          lFullResults.AddRange( lChunck.Results );
        }
        string lStrResult = string.Join( ", ", lFullResults );
        string lTimeMsg   = $"Calculate all primes up to {_MaxValueToTest.ToString("#,##0")} with \r\nMultiple Background Workers with {_NumberOfChuncks} workers: Total duration (seconds): {_Stopwatch.ElapsedMilliseconds/1000}";
        txt_Output.Text   = $"{lTimeMsg}\r\n{lStrResult}";
        _Workers = null;
      }
    }
    private bool IsPrimeNumber             ( long aValue )
    {
      // see https://en.wikipedia.org/wiki/Prime_number
      // Among the numbers 1 to 6, the numbers 2, 3, and 5 are the prime numbers, while 1, 4, and 6 are not prime.
      if ( aValue <= 1 ) return false;
      if ( aValue == 2 ) return true ;
      if ( aValue == 3 ) return true ;
      if ( aValue == 4 ) return false;
      if ( aValue == 5 ) return true ;
      if ( aValue == 6 ) return false;
      bool       lIsPrimeNumber = true;
      long       lMaxTest       = aValue / 2 + 1;
      for ( long lTest          = 2; lTest < lMaxTest && lIsPrimeNumber; lTest++ )
      {
        long lMod = aValue % lTest;
        lIsPrimeNumber = lMod != 0;
      }
      return lIsPrimeNumber;
    }
  }
  public class WorkChunk
  {
    public int       StartRange { get; set; }
    public int       EndRange   { get; set; }
    public List<int> Results    { get; set; }
    public string    Message    { get; set; }
    public DateTime  StartTime  { get; set; } = DateTime.MinValue;
    public DateTime  EndTime    { get; set; } = DateTime.MinValue;
    public override string ToString()
    {
      StringBuilder lResult = new StringBuilder();
      lResult.Append( $"WorkChunk: {StartRange} to {EndRange}" );
      if ( Results    == null                   ) lResult.Append( ", no results yet" ); else lResult.Append( $", {Results.Count} results" );
      if ( string.IsNullOrWhiteSpace( Message ) ) lResult.Append( ", no message"     ); else lResult.Append( $", {Message}" );
      if ( StartTime  == DateTime.MinValue      ) lResult.Append( ", no start time"  ); else lResult.Append( $", Start: {StartTime.ToString("HH:mm:ss.ffff")}" );
      if ( EndTime    == DateTime.MinValue      ) lResult.Append( ", no end time"    ); else lResult.Append( $", End: {  EndTime  .ToString("HH:mm:ss.ffff")}" );
      return lResult.ToString();
    }
  }
}
Community
  • 1
  • 1
  • Could you just use [`Parallel.For`](https://msdn.microsoft.com/en-us/library/system.threading.tasks.parallel.for.aspx)? – Blorgbeard Aug 18 '16 at 23:56
  • 1
    Your question is too broad, and frankly is already addressed by a number of other Stack Overflow questions there were asked, in spite of also being too broad. The current equivalent to `BackgroundWorker` is to use `Task.Run()` (replaces `DoWork` event) and `Progress` (replaces `ProgressChanged` event). With `async`/`await`, you also get `RunWorkerCompleted` functionality. You can still use `BackgroundWorker` if you want. All techniques default to using the thread pool, so they will schedule on existing cores equivalently. TPL-based solutions have more control over this though, if you need. – Peter Duniho Aug 19 '16 at 00:00
  • Hi All, I would like to thank Blorgbead and Peter Duniho for their feedback – Thank You. Now I want to ask what the @#$%^&! How do you ask a general architecture question without a broad question??? **So, I repeat my questions:** - Is Background worker the right technique to use for a CPU intensive task like this? - If not, what technique is better? - Are there any good examples out there for a CPU intensive task like this? - What risks am I taking if I use Background worker? Regards Greg Harris – Greg Harris Aug 19 '16 at 08:44

1 Answers1

1

I will have up to 1 million business objects open at once

Sure, but you won't be displaying that many on the screen all at once.

Also, this needs to be as close to real time as possible without blocking the UI.

The first thing to check is if it's fast enough already. Given a realistic number of objects on reasonable hardware, can you filter fast enough directly on the UI thread? If it's fast enough, then it doesn't need to be faster.

I find many posts that say I should replace BackgroundWorker with the new async await c# commands.

async is not a replacement for BackgroundWorker. However, Task.Run is. I have a blog post series that describes how Task.Run is superior to BackgroundWorker.

Performance dies when you have too many progress notifications back to the UI thread.

I prefer solving this in the UI layer, using something like ObserverProgress.

Is Background worker the right technique to use for a CPU intensive task like this?

Before jumping to a multithreading solution, consider virtualization first. As I mentioned in the beginning, you can't possibly display that many items. So why not just run the filter until you have enough to display? And if the user scrolls, then run the filter some more.

what technique is better?

I recommend:

  1. Test first. If it's fast enough to filter all items on the UI thread, then you're already done.
  2. Implement virtualization. Even if filtering all items is too slow, filtering only some items until you have enough to display may be fast enough.
  3. If neither of the above are fast enough, then use Task.Run (with ObserverProgress) in addition to the virtualization.
Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810