.NET 2.0中真正的多线程实例
多线程实例源代码(保留原文处链接)
http://www.codeproject.com/useritems/RealMultiThreading/RealMultiThreading_src.zip
简介
多线程总是那么让人振奋。大家都希望能够同时处理很多事情,不过如果我们没有正确的硬件的话,我们很难达到这点。到目前为止,我们所做的只是分开CPU使用较多的工作,使其为后台进程,这样可以使得界面上不被阻塞。
不过我希望能够得到更好的效果,并充分利用当前最新的多CPU效能。因此,我将写一个真正的多线程实例,将会有多个线程作为后台线程在运行。
这就是这篇文章将要写的,不得不说的是,最终的结果实在是让我很激动。希望你也能够发觉它的用处。
在有4个CPU的多CPU服务器上,我得到了280%的效果(测试的是CPU型的任务),在一些非CPU占用较多的任务中,它可以提高到500% 到1000%的性能。
背景
网上也有不少介绍.Net 2.0下的多线程的文章,应该说,我从它们中受益颇多。我正在使用的是BackgroundWorker .Net 2.0组件(不过也有实现在.net 1.1下的代码)。
这里,我列出一些有用的文章链接:
来自Paul Kimmel的很好的介绍性文章 http://www.informit.com/articles/article.asp?p=459619&seqNum=5&rl=1
来自Juval Löwy的介绍性文章 http://www.devx.com/codemag/Article/20639/1954?pf=true
(必看)Joseph Albahari的C#中使用线程 http://www.albahari.com/threading/part3.html
Michael Weinhardt写的在Windows Forms 2.0中一个简单安全的多线程,我使用了这个网页中的CPU密集型任务,这是他从Chris Sell的文章中引用的。
http://www.mikedub.net/mikeDubSamples/SafeReallySimpleMultithreadingInWindowsForms20/SafeReallySimpleMultithreadingInWindowsForms20.htm
如果你对多线程世界仍然不是特别熟悉或者希望了解最新的.Net 2.0的 BackgroundWorker组件,那么应该好好读读上面的文章。
提出的问题
任何一个任务……无论是CPU密集型还是普通的任务:
CPU密集型:它可以分成一个、两个或多个线程,每个线程会占用一个CPU(这样就使得程序的性能翻番)
普通任务:每一个顺序执行的普通任务,在进行数据存储或使用一个web service的时候都会有一些延迟。所有的这些,都意味着这些没有使用的时间对于用户或任务本身来说有了浪费。这些时间将被重新安排,并将被并行的任务使用,不会再丢失。也就是说,如果有100个100ms延迟的任务,它们在单线程模型和20个线程模型的性能差距会达到1000%。
我们说,如果要处理一个创建一个网站多个块的任务,不是顺序的执行,而是花1-4秒钟把所有的section创建好;商标,在线用户,最新文章,投票工具等等…… 如果我们能够异步地创建它们,然后在发送给用户,会怎么样?我们就会节省很多webservice的调用,数据库的调用,许多宝贵的时间……这些调用会更快地执行。看上去是不是很诱人?
解决方案如下:
调用BackgroundWorker,正如我们想要的那样,我们会继承它。后台worker会帮助我们建立一个“Worker”,用于异步地做一个工作。
我们想做的是建立一个工厂Factory(只是为了面向对象的设计,于设计模式无关),任务会放在在这个Factory中执行。这意味着,我们将有一类的任务,一些进程,一些知道如何执行任务的worker。
当然我们需要一个负责分配任务给这些worker的manager,告诉这些worker当它们做完一步或全部时,做什么事情。当然,我们也需要manager能够告诉worker停止当前的任务。它们也需要休息啊:)当manager说停止的时候,它们就应该停止。
我们将会从底至上地解释这些,首先从Worker说起,然后再继续Manager。
Worker
它是Background worker的继承类,我们构建一个构造函数,并分配两个BackgroundWorker的属性,分别是WorkerReportsProgress 和WorkerSupportsCancellation,它们的功能就向其名字的意义一样:报告进度,停止任务。每个Worker还有一个id,Manager将会通过这个id控制它们。
public class MTWorker : BackgroundWorker
{
#region Private members
private int _idxLWorker = 0;
#endregion
#region Properties
public int IdxLWorker
{
get { return _idxLWorker; }
set { _idxLWorker = value; }
}
#endregion
#region Constructor
public MTWorker()
{
WorkerReportsProgress = true;
WorkerSupportsCancellation = true;
}
public MTWorker(int idxWorker)
: this()
{
_idxLWorker = idxWorker;
}
#endregion
另外,我们将重载BackgroundWorker的一些函数。事实上,最有意思的是,究竟谁在做真正的工作?它就是OnDoWork,当我们invoke或者启动多线程的时候,它就会被调用。在这里,我们启动任务、执行任务、取消和完成这个任务。
我加了两个可能的任务,一个是普通型的,它会申请并等待文件系统、网络、数据库或Webservices的调用。另一个是CPU密集型的任务:计算PI值。你可以试试增加或减少线程数量后,增加或是减少的延迟(我的意思是增减Worker的数量)
OnDoWork方法的代码
protected override void OnDoWork(DoWorkEventArgs e)
{
//Here we receive the necessary data for doing the work...
//we get an int but it could be a struct, class, whatever..
int digits = (int)e.Argument;
double tmpProgress = 0;
int Progress = 0;
String pi = "3";
// This method will run on a thread other than the UI thread.
// Be sure not to manipulate any Windows Forms controls created
// on the UI thread from this method.
this.ReportProgress(0, pi);
//Here we tell the manager that we start the job..
Boolean bJobFinished = false;
int percentCompleteCalc = 0;
String TypeOfProcess = "NORMAL"; //Change to "PI" for a cpu intensive task
//Initialize calculations
while (!bJobFinished)
{
if (TypeOfProcess == "NORMAL")
{
#region Normal Process simulation, putting a time
delay to emulate a wait-for-something
while (!bJobFinished)
{
if (CancellationPending)
{
e.Cancel = true;
return; //break
}
//Perform another calculation step
Thread.Sleep(250);
percentCompleteCalc = percentCompleteCalc + 10;
if (percentCompleteCalc >= 100)
bJobFinished = true;
else
ReportProgress(percentCompleteCalc, pi);
}
#endregion
}
else
{
#region Pi Calculation - CPU intensive job,
beware of it if not using threading ;) !!
//PI Calculation
if (digits > 0)
{
pi += ".";
for (int i = 0; i < digits; i += 9)
{
// Work out pi. Scientific bit :-)
int nineDigits = NineDigitsOfPi.StartingAt(i + 1);
int digitCount = System.Math.Min(digits - i, 9);
string ds = System.String.Format("{0:D9}", nineDigits);
pi += ds.Substring(0, digitCount);
// Show progress
tmpProgress = (i + digitCount);
tmpProgress = (tmpProgress / digits);
tmpProgress = tmpProgress * 100;
Progress = Convert.ToInt32(tmpProgress);
ReportProgress(Progress, pi);
// Deal with possible cancellation
if (CancellationPending) //If the manager says to stop, do so..
{
bJobFinished = true;
e.Cancel = true;
return;
}
}
}
bJobFinished = true;
#endregion
}
}
ReportProgress(100, pi); //Last job report to the manager ;)
e.Result = pi; //Here we pass the final result of the Job
}
Manager
这是一个很有趣的地方,我确信它有很大的改进空间-欢迎任何的评论和改进!它所做的是给每个线程生成和配置一个Worker,然后给这些Worker安排任务。目前,传给Worker的参数是数字,但是它能够传送一个包含任务定义的类或结构。一个可能的改进是选择如何做这些内部工作的策略模式。
调用InitManager方法配置任务,和它的数量等属性。然后,创建一个多线程Worker的数组,配置它们。
配置的代码如下:
private void ConfigureWorker(MTWorker MTW)
{
//We associate the events of the worker
MTW.ProgressChanged += MTWorker_ProgressChanged;
MTW.RunWorkerCompleted += MTWorker_RunWorkerCompleted;
}
Like this, the Worker’s subclassed thread management Methods are linked to the Methods held by the Manager. Note that with a Strategy pattern implemented we could assign these to the proper manager for these methods.
最主要的方法是AssignWorkers,它会检查所有的Worker,如果发现没有任务的Worker,就分配一个任务给它。直到扫描一遍之后,没有发现任何有任务的Worker,这样就意味这任务结束了。不需要再做别的了!
代码如下:
public void AssignWorkers()
{
Boolean ThereAreWorkersWorking = false;
//We check all workers that are not doing a job... and assign a new one
foreach (MTWorker W in _arrLWorker)
{
if (W.IsBusy == false)
{
//If there are still jobs to be done...
//we assign the job to the free worker
if (_iNumJobs > _LastSentThread)
{
//We control the threads associated to a worker
//(not meaning the jobs done) just 4 control.
_LastSentThread = _LastSentThread + 1;
W.JobId = _LastSentThread; //We assign the job number..
W.RunWorkerAsync(_iPiNumbers); //We pass the parameters for the job.
ThereAreWorkersWorking = true;
//We have at least this worker we just assigned the job working..
}
}
else
{
ThereAreWorkersWorking = true;
}
}
if (ThereAreWorkersWorking == false)
{
//This means that no worker is working and no job has been assigned.
//this means that the full package of jobs has finished
//We could do something here...
Button BtnStart = (Button)FormManager.Controls["btnStart"];
Button BtnCancel = (Button)FormManager.Controls["btnCancel"];
BtnStart.Enabled = true;
BtnCancel.Enabled = false;
MessageBox.Show("Hi, I'm the manager to the boss (user): " +
"All Jobs have finished, boss!!");
}
}
只要有任务完成,这个方法就会被调用。从而,保证所有的任务能够完成。
我们还通过一个属性链接到Form上,这样我们就能向UI上输出我们想要的任何消息了。当然,你可能想链接到其它的一些类,不过这是最基本最通用的。
Well… improving it we could get a BackgroundManager for all our application needs..
界面
连接到界面上,并不是最主要的功能。这一部分的代码量非常少,也很简单:在Manager中添加一个引用,并在form的构造函数中配置它。
在一个按钮中,执行Manager类的LaunchManagedProcess方法。
private MTManager LM;
public Form1()
{
InitializeComponent();
LM = new MTManager(this, 25);
LM.InitManager();
}
private void btnStart_Click(object sender, EventArgs e)
{
btnStart.Enabled = false;
btnCancel.Enabled = true;
LM.LaunchManagedProcess();
}
private void btnCancel_Click(object sender, EventArgs e)
{
LM.StopManagedProcess();
btnCancel.Enabled = false;
btnStart.Enabled = true;
}
(下面的自己看喽:)
Trying it!
This is the funniest part, changing the properties of how many threads to run simultaneously and how many Jobs to be processed and then try it on different CPU’s… ah, and of course, change the calculation method from a CPU-intensive task to a normal task with a operation delay...
I would love to know your results and what have you done with this, any feedback would be great!!
Exercises For You…
This is not done! It could be a MultiThreadJob Framework if there is being done the following:
Implement a Strategy pattern that determines the kind of Worker to produce (with a factory pattern) so we will be able to do different kind of jobs inside the same factory.. what about migrating a database and processing each table in a different way… or integrating systems with this engine…
Implement -or extend- the strategy pattern for determining the treatment for the Input data and the result data of the jobs. We could too set-up a factory for getting the classes into a operating environment.
Optimize the AssignWorkers engine – I am pretty sure it can be improved.
Improve the WorkerManager class in order to be able to attach it to another class instead to only a form.
Send me the code! I Would love to hear from you and what have you done.
About Jose Luis Latorre
Professional developer since 1991, having developed on multiple systems and languages since then, from Unix, as400, lotus notes, flash, javascript, asp, prolog, vb, c++, vb.Net, C#...
Now I'm focused on .Net development, both windows and web with two-three year experience on both and fully up-to date with 2.0 .Net in both Vb and C#
Also have experience with SQL server 2005 and Business Intelligence.
Jose Luis Lives in Barcelona, Spain, with his cat Pancho. To contact Jose Luis, email him at
joslat@gmail.com.
Click here to view Jose Luis Latorre's online profile.
来源:http://www.msproject.cn/Article/RealMultiThreading.aspx
浙公网安备 33010602011771号