上一期笔者介绍了Silverlight实现多线程的诸多解决方案,本期笔者将通过一个实例来实现所有多线程编程方法,并且还将于JavaScript和Flash两种Web客户端技术性能进行比较,请勿拍砖。
\在正式编程前,笔者还要重申上期非常重要的观点:Silverlight多线程主要作用不是在于提高性能,而是在于用户体验。这里要给多线程泼一盆冷水了,多线程与性能提升不是正比关系,如果你使用一个单核CPU的客户端设备,那么即便你创建100个多线程也与单线程的计算性能是一样的,因为一个CPU时间片下只能处理一个线程,多线程也必须串行处理,甚至还可能因为过多的CPU调度开销而导致性能不及单线程的情况。当然在多核的情况下多线程可以负载到多个CPU上并行执行而提升性能,经过笔者在项目实施前的技术研究中发现如果客户端有N核的情况下,Silverlight多线程可以被N个CPU时间片平分,而CLR将同时让N+1个线程处于Ready状态,经过反复测试多线程性能是单线程的近N倍。其实客户端已经呈现多核趋势,就在不久前发布了PSP的下一代产品NGP采用ARM 4核处理器,而iPad2采用A5双核处理器,而我们现在用的笔记本与台式机基本都是超过2核的处理器,所以多线程的计算能力还是很有前景的。
\下面我们就一起来看看实例,这个实例笔者选择了比较容易懂的素数计数函数(Prime-counting function)作为实例,用数学专业术语来说就是π(x),有没有搞错怎么和圆周率有关?这里不是圆周率而是π函数,是一个用来表示小于或等于某个实数x的素数的个数的函数。比如π(10)=4,因为不大于10的素数有2,3,5,7共计4个。对于π(x)的确定性算法笔者准备了两种:
\- 试除法\
具体方法是从3开始对所有不大于x的奇数进行素数判断。当判断i是否为素数时,通过从3开始到i的平方根(i=m*n中必然有一个因子小于i的平方根)的所有奇数进行试除,如果i能被整除则i不是素数,否则i是素数。该算法最易理解,而且可以并行试除,并行试除法的思路是按照2k*m+n的同余类进行分组,如果有k个并行组,那么对于从3开始对所有不大于x的奇数可以用{2k*m+1,2k*m+3,…,2k*m+2k-1}共k个同余组来分别进行试除,最后π(x)等于所有分组素数求和。
\ 埃拉托斯特尼筛法\埃拉托斯特尼筛法,简称埃氏筛或爱氏筛,是一种由古希腊数学家埃拉托斯特尼所提出的一种简单检定素数的算法,该算法的思路从第一个素数开始,按照素数的倍数都是合数的思路,全部筛去,然后再筛去第二个素数的倍数,一直到当前素数大于x的平方根时结束,所得到没有筛去的数都是素数。该算法是已知确定性算法中时间复杂度最低的算法,但缺点是不能并行(至少笔者目前还没有找到并行筛法,如果你找到了请与笔者联系)。
\在本案例中笔者使用试除法进行多线程计算,并通过筛法来校验计算的正确性。下面我们首先实现Silverlight的两个算法类:
\- 试除类PrimeFinder\
该类主要负责对并行算法的支持,其中MaxPrime属性用来记录最大素数,PrimeCount属性记录素数个数,Stat属性的类型为枚举类WorkerStat { Init, Working, Worked },用以监视线程的工作状态。OnFindComplete事件用于通知UI线程查找完成。其中主要函数实现如下:
\\publicvoid FindPrime() \{ \\ _primeCount = 0; \ _stat = WorkerStat.Working; \\ for (uint i = _startNum; i \u0026lt;= _maxNam; i += _step)\ { \\ if (IsPrime(i)) \ { \ _primeCount++; \ _maxPrime = i; \ } \ } \ _stat = WorkerStat.Worked; \\ //通知完成查找 \ InvokeFindComplete(EventArgs.Empty); \} \\privatebool IsPrime(uint x) \{ \\ if (x == 1u) returnfalse; \ uint sqrtx = (uint)(Math.Sqrt(x)); \ for (uint i = 3u; i \u0026lt;= sqrtx; i += 2u) \ { \ if (x % i == 0) returnfalse; \ } \ return true; \}\\
其中FindPrime方法用于查找记录素数,查找起始点从_startNum开始,步进为_step。而IsPrime方法用于判断是否是素数,遍历从3开始到x的平方根的所有奇数能否整除x。
\ 埃拉托斯特尼筛法类EratosthenesFinder\该类用于验证多线程下试除法查找的结果是否正确,其中主要函数FindPrime实现如下:
\\publicvoid FindPrime(uint MaxNumber) \{ \ _primeCount = 1; \ bool[] Numbers = newbool[MaxNumber]; \ uint SqrtMaxNumber = (uint)(Math.Sqrt(MaxNumber -= 1u)); \ for (uint i = 1u; i \u0026lt; SqrtMaxNumber; ) \ { \\ while (Numbers[i += 2u]) ; \ for (uint k = ((uint)(MaxNumber / i) + 1u) | 1u; k \u0026gt; i; )\ { \ while (Numbers[k -= 2u]) ; \ Numbers[k * i] = true; \ } \ } \\ for (uint i = 3u; i \u0026lt; MaxNumber; i += 2u) \ if (!Numbers[i]) \ { \ _primeCount++; \ _maxPrime = i; \ } \\ //通知完成查找 \ InvokeFindComplete(EventArgs.Empty);\}\\
在主页面上,笔者创建了6个按钮执行不同的调用方式:
\- 后台单线程执行Eratosthenes筛法进行验证\
- 在UI线程直接查找\
- 在后台单线程查找\
- 基于ThreadPool的后台多线程查找\
- 基于BackgroundWorker的后台多线程查找\
- 基于Thread的后台多线程查找\
而对于DispatchTimer类,笔者将其作为定时器用于对后台线程状态的监控。具体方法如下:
\在主页面中加入定时器属性dt,并在页面加载事件中定义定时器时歇与Tick事件:
\\DispatcherTimer dt = newDispatcherTimer(); \void MainPage_Loaded(object sender, RoutedEventArgs e) \{ \\ …… \ dt.Interval = newTimeSpan(0, 0, 0, 0, 10); \ dt.Tick += newEventHandler(dt_Tick); \} \\void dt_Tick(object sender, EventArgs e) \{ \\ tbTimer.Text = (Environment.TickCount - StartTickCount).ToString(); \\ switch (CurrFinder) \ { \\ caseUseFinder.EratosthenesFinder: \ tbPrimeCount.Text = (efinder.PrimeCount).ToString();\ break; \ caseUseFinder.SinglePrimeFinder: \ tbPrimeCount.Text = (singlefinder.PrimeCount + 1).ToString(); \ break; \ caseUseFinder.MultiPrimeFinder: \ tbPrimeCount.Text = (multifinder.Sum(t =\u0026gt; t.PrimeCount) + 1).ToString(); \ tbIdleThread.Text = multifinder.Count(t =\u0026gt; t.Stat == WorkerStat.Init).ToString();//闲置线程数 \ tbWorkingThread.Text = multifinder.Count(t =\u0026gt; t.Stat == WorkerStat.Working).ToString();//工作线程数 \ tbWorkedThread.Text = multifinder.Count(t =\u0026gt; t.Stat == WorkerStat.Worked).ToString();//完成线程数 \ break \ …… \ } \} \\
dt_Tick事件定义了单线程与多线程下对查找过程和多线程工作状态的显示。而在所有查找方法调用前触发dt.Start()事件开始监控;在完成查找时,调用dt.Stop()事件停止监控。
\下面鉴于篇幅所限,我主要介绍基于ThreadPool的后台多线程查找的实现方式:
\\privatevoid btnMultiWorker_Click(object sender, System.Windows.RoutedEventArgs e) \{ \\ //设置当前查找器 \ CurrFinder = UseFinder.MultiPrimeFinder; \ gdMultiThreadInfo.Visibility = Visibility.Visible; \ multifinder.Clear(); \ CallBackThreadCount = 0; \ StartTickCount = Environment.TickCount; \ dt.Start(); \\ //构造多线程查找器 \ for (uint i = 0; i \u0026lt; ThreadCount; i++) \ { \ //对余2i+1的同余类进行查找,步进为两倍线程数 \ PrimeFinder afinder = newPrimeFinder(2 * i + 1, MaxNum, 2 * ThreadCount); \ afinder.OnFindComplete += (s1, e1) =\u0026gt; Dispatcher.BeginInvoke(multifinder_OnFindComplete); \ multifinder.Add(afinder); \ ThreadPool.QueueUserWorkItem(state =\u0026gt; afinder.FindPrime(), i); \ } \} \\void multifinder_OnFindComplete() \{ \\ CallBackThreadCount++; \\ //当全部完成时显示结果 \ if (CallBackThreadCount == ThreadCount)\ { \ dt.Stop(); \ tbLog.Text += string.Format(\"后台多线程试除法找到{0}内的素数共{1}个,最大素数为{2},耗时{3}毫秒(基于ThreadPool)\\