日期:2011-06-18 浏览次数:20408 次
说实在的,我最初打算做的事情和本文主要讨论的内容毫不相关。那时,我第一次发现我需要在.NET中计算一个圆的面积,当然,首先需要一个pi(π)的精确值。System.Math.PI用起来倒是很方便,但它只提供了20位的精度,我不禁为计算的精度而担心(其实21位的就可以绝对令我感到舒服)。所以和其他任何称职的程序员一样,我忘记了真正需要解决的问题,而埋头写出了一个自己喜欢的可以算出任意位小数的π值的程序。最终的结果如图1。
图1. 计算Pi值的程序
耗时操作(Long-Running Operations)的进度
虽然大多数的程序不需要计算pi的值,但是很多的程序都需要进行一些耗时的操作,比如打印、调用一个Web service或者计算一位太平洋西北岸亿万富翁的利息收入。对用户来说,只要可以看到当前完成的进度,这样的等待通常还是可以接受的,甚至是一个抽身忙些其他事情的机会。所以我给我的每一个小程序都添加了一个进度条(progress bar)。我的这个计算pi值的程序所用的算法每次计算9位数字。一旦新的一组数字被计算出来,我的程序就更新TextBox控件并移动ProgressBar来显示我们的进度。比如图2就是正在计算1000位pi值的情形(如果21位没有问题,1000位也必定更好)。
图2. 正在计算1000位Pi值
下面的代码展示了pi值的数字被计算出来之后,用户界面(UI)是如何更新的:
void ShowProgress(string pi, int totalDigits, int digitsSoFar) {
_pi.Text = pi;
_piProgress.Maximum = totalDigits;
_piProgress.Value = digitsSoFar;
}
void CalcPi(int digits) {
StringBuilder pi = new StringBuilder("3", digits + 2);
// 显示进度
ShowProgress(pi.ToString(), digits, 0);
if( digits > 0 ) {
pi.Append(".");
for( int i = 0; i < digits; i += 9 ) {
int nineDigits = NineDigitsOfPi.StartingAt(i+1);
int digitCount = Math.Min(digits - i, 9);
string ds = string.Format("{0:D9}", nineDigits);
pi.Append(ds.Substring(0, digitCount));
// 显示进度
ShowProgress(pi.ToString(), digits, i + digitCount);
}
}
}
一切进行的都很顺利,直到我在计算pi的1000位的过程中切换到了其他程序做了些什么然后再切换回来时……。我看到了图3显示的画面:
图3. 没有paint事件了
问题的根源当然在于我们的程序是单线程的。当这个线程忙于计算pi值时,就没有机会去绘制UI了。之前我之所以没有遇到这个问题是因为当我设置TextBox.Text属性和ProgressBar.Value属性时,这些控件,作为设置属性操作的一部分,可以强制他们的绘画操作立即进行(尽管我注意到了progress bar的情况要比text box好一些)。然而,当我把这个程序放到后台然后再带回前台时,我需要重绘整个客户区,对窗口来说就是一个Paint事件。因为当前正在处理的事件(Calc按钮的Click事件)返回之前其他事件是不会得到处理的,于是我们就没有机会看到更多的进度显示了。我现在需要做的就是把UI线程释放出来专做UI工作,把耗时的操作放到后台处理。为了实现这个目标,我们需要另外一个线程。
异步操作
现在我的同步Click handler看起来是这样的:
void _calcButton_Click(object sender, EventArgs e) {
CalcPi((int)_digits.Value);
}
回忆一下我们的问题是“直到CalcPi返回,线程才可以从Click handler返回,窗口才有机会处理Paint(或其他)事件”。处理这种情况的一个方法是启动另外的线程,比如:
using System.Threading;
int _digitsToCalc = 0;
void CalcPiThreadStart() {
CalcPi(_digitsToCalc);
}
void _calcButton_Click(object sender, EventArgs e) {
_digitsToCalc = (int)_digits.Value;
Thread piThread = new Thread(new ThreadStart(CalcPiThreadStart));
piThread.Start();
}
现在,在button Click事件返回之前无需再等待CalcPi的完成了,我创建并启动了一个新的线程,Thread.Start方法将调度并启动新的线程,然后立即返回,让UI线程回到它自己的工作上来。现在如果用户和程序交互(比如放到后台,置到前台,改变窗口大小,关闭它),UI线程可以自由地处理这些事件,同时worker线程也在进行它的计算pi的工作。图4显示了两个线程工作的情形:
图4. 幼稚的多线程
你也许注意到了,我没有为worker线程的入口点——CalcPiThreadStart传递任何参数,而是把需要计算的位数放入一个字段(field)_digitsToCalc中,调用线程的入口点,CalcPi被调用。这是一种痛苦,也是我喜欢用Delegate来作异步工作的一个原因。Delegate支持参数,避免了我对增加一个额外的临时field和一个额外的函数而做激烈的思想斗争。
如果你对delegate(委托)不熟悉,就认为他们是调用static或instance函数的对象。在C#中它们的声明语法和函数是一样的。比如CalcPi的一个委托看起来是这样的:
delegate void CalcPiDelegate(int digits);
一旦有了一个委托,我就可以实例化一个对象来同步调用CalcPi函数:
void _calcButton_Click(object sender, EventArgs e) {
CalcPiDelegate calcPi = new CalcPiDelegate(CalcPi);
calcPi((int)_digits.Value);
}
当然,我并不想同步调用CalcPi;我想要异步调用它。然而,在我做这个之前我们需要了解一点委托的工作原理。上面的委托声明实际上声明了一个类,该类继承自一个自带有三个函数(Invoke、BeginInvoke和EndInvoke)的MultiCastDelegate类,像这样:
class CalcPiDelegate : MulticastDelegate {
public void Invoke(int digits);
public void BeginInvoke(int digits, AsyncCallback callback,
&nb