我有一个WinForms C#程序,我将在用户的机器上同时打开多达100万个业务对象(在内存中)。
我的经理要求对这些业务对象进行真正简单的过滤。 因此,如果您过滤" Fred",将向用户显示包含" Fred"的所有对象的列表。在任何文本字段(名称,地址,联系人等)。 此外,这需要尽可能接近实时而不阻止UI。 所以,如果你输入" Fred"进入过滤器文本框,只要" F"键入后,搜索将开始使用" F"在任何文本字段中(我认为我可能会在搜索中坚持至少3个字符)。 当文本框更改为" Fr"时,将停止旧搜索(如果仍在执行)并开始新搜索。
这是用户本地计算机上CPU占用率极高的操作,IO为零。 这听起来像我应该启动单独的任务来在我的CPU上的不同核心上的不同线程上运行。 完成所有操作后,将结果合并回一个列表并将结果显示给用户。
我老了,这听起来像是BackgroundWorker的工作,但我读到在.NET 4.5中显然将BackgroundWorker标记为过时(悲伤的面孔)。请参阅:Async/await vs BackgroundWorker
我发现很多帖子都说我应该用新的异步等待c#命令替换BackgroundWorker。
但是,这方面很少有很好的例子,而且我发现了" async await的注释不能保证单独的线程"并且所有示例都显示了等待任务的IO /网络密集型任务(而不是CPU密集型任务)。
我找到了一个很好的BackgroundWorker示例,它寻找质数,这是一个类似的CPU密集型任务,我玩弄了它,发现它可以满足我的大部分需求。但我遇到的问题是BackgroundWorker在.NET 4.5中已经过时了。
BackgroundWorker调查的结果是:
问题:
后台工作者是否适合用于此类CPU密集型任务? 如果没有,哪种技术更好? 有没有像这样的CPU密集型任务的好例子? 如果我使用背景工作者,我会冒什么风险?
基于单个后台工作程序的代码示例
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;
}
}
}
基于多个后台工作人员的代码示例
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();
}
}
}
答案 0 :(得分:1)
我将一次打开多达100万个业务对象
当然,但你不会一次在屏幕上显示。
此外,这需要尽可能接近实时而不会阻止UI。
要检查的第一件事是它是否足够快。给定合理硬件上的实际数量的对象,您可以直接在UI线程上过滤得足够快吗?如果它足够快,那么它不需要更快。
我发现很多帖子都说我应该用新的异步等待c#命令替换BackgroundWorker。
async
不是BackgroundWorker
的替代品。但是,Task.Run
是。我有一篇描述how Task.Run
is superior to BackgroundWorker
的博客文章系列。
如果有太多进度通知回到UI线程,性能就会消失。
我更喜欢在UI层中使用ObserverProgress
。
背景工作者是否适合用于像这样的CPU密集型任务?
在跳转到多线程解决方案之前,首先考虑虚拟化。正如我在开头提到的那样,你不可能显示那么多项目。那么为什么不运行过滤器直到你有足够的显示?如果用户滚动,则再运行一次过滤器。
什么技术更好?
我建议:
Task.Run
(ObserverProgress
)。