How to parallel execute tasks on separate CPU cores

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 rnSingle Background Worker with only 1 worker: Total duration (seconds): {_Stopwatch.ElapsedMilliseconds/1000}";
          txt_Output.Text   = $"{lTimeMsg}rn{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 rnMultiple Background Workers with {_NumberOfChuncks} workers: Total duration (seconds): {_Stopwatch.ElapsedMilliseconds/1000}";
            txt_Output.Text   = $"{lTimeMsg}rn{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();
        }
      }
    }
    

    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:

  • Test first. If it's fast enough to filter all items on the UI thread, then you're already done.
  • Implement virtualization. Even if filtering all items is too slow, filtering only some items until you have enough to display may be fast enough.
  • If neither of the above are fast enough, then use Task.Run (with ObserverProgress ) in addition to the virtualization.
  • 链接地址: http://www.djcxy.com/p/50170.html

    上一篇: 与Task Parallel并行相比,并行管道的优势是什么?

    下一篇: 如何在单独的CPU内核上并行执行任务