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:
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:
Task.Run
(with ObserverProgress
) in addition to the virtualization. 上一篇: 与Task Parallel并行相比,并行管道的优势是什么?
下一篇: 如何在单独的CPU内核上并行执行任务