多线程并行运算研究(转)

多核时代,多线程是大势所趋。最近用C#写了个程序,用多线程并发技术计算质数列。但看似很容易的多线程应用,处理起来还是非常复杂。下面是开发过程中发现的问题和积累的心得。

 

计算质数,最简单的方法就是试整除:如果一个大于1的整数,不能被小于它开根号的任何一个质数整除,则它是质数。我也以这个基本算法作为设计基础,而不是筛选法。

有关计算质数列的算法就不再详述,本文主要研究多线程计算质数列。

 

多线程计算质数列最直观的想法,就是多个线程同时试除各个整数。

开始我用了个非常笨的方法,我为试除每个整数新开一个线程,并且让总线程数保持在一定范围。但后来发现效率非常低,原因是开始、撤销线程的开销太大。

相信大多数人都不会像我这么笨,都会想到这个办法:首先设置质数列第一个元素是2,然后设置一个变量N,初始化为3,然后各个线程利用同步锁互斥地访问、累加变量N获得要试除的数,每证实一个数是质数,就添加到质数列。

 

首先遇到一个小问题:单线程下,试除一个数时很容易可以保证已经找到所有小于它开根号的所有质数,只要是简单的从小到大递推算法就可以保证这点了,但在多线程下如何保证?

无法保证……

简单的解决方法是,让这个线程先挂起,等其它线程找到已经比它开根号小的所有质数,再继续运算。

初始化整个质数列数组所有元素为0,每计算一个质数就写到数组指定位置,试除的时候,如果下标指向一个为0的单元,则当前线程挂起等待,直到这个单元不为0。当被试除的整数比增大时,出现这种情况会越来越少,所以对性能几乎没有影响,而且当CPU个数少于线程数时,挂起线程还会给其它线程让出CPU计算时间。

 

问题又来了,你怎么知道找到一个质数后它应该放在哪个位置?

同样是单线程下不存在的问题,单线程计算质数只需要放在上次放质数位置的下一个位置就可以了。

多线程下,你无法保证获得质数是从小到大的。如果一个试除比较大整数的线程先完成试除任务,并证实是质数,但试除比较小整数的线程还在运算,那这个较大的质数怎么办呢?明显,当你试除获得一个质数后,你要等其它所有线程正在试除的整数都比它大的时候,才能才能确定这个质数在数组里的位置。

如何解决?等?我想这不同于第一个问题,等是一个非常笨的方法,因为无论当前试除的整数有多大,这种等待都会频频出现,会严重影响性能,完全没法发挥多核的性能。实验证明,这种方法的效率甚至比单线程还低。

 

所以不能等,否则就失去多线程的意义了。线程要继续试除,并将计算得出的质数先放起来并排好序,称他们为“待写入质数集合”,如果所有线程试除的整数都大于这个待写入质数集合中的某些元素,那么这些元素就可以被正式写入到质数列数组中了。

 

OK,说做就做……开一个这样的空间,.NET里面有个System.Collections.SortedList的类可以满足这个需求,就它了。

每找到一个质数就把它写到这个SortedList里面(记得用同步锁),会被自动排序,然后定期扫描各个线程当前试除的最小整数m,把SortedList里面小于m的质数都添加到质数序列中。

程序好不容易写出来,开4个线程,果然占了100%的CPU(4核CPU),经过检验计算正确。

然后试用2个线程,不出所料地占了50%的性能,但计算耗时出来后就傻眼了……居然比4线程还快……

来到这个时候就相当郁闷了……

找原因!

找了半天,找到SortedList头上:如果SortedList储存的元素太多,插入、取出元素的开销就会很大。但当初想到的是:假设多个线程的计算速度是基本一样的,那样4个线程试除的整数都不会相差太多,4个线程几乎同步推进,SortedList里面的元素个数就不会太多。但事实证明我错了,我写了些代码检查SortedList里面的元素个数,当开了4个线程的时候,SortedList里面最多的元素个数居然有几千!就是说,有的时候,某个线程还在验证一个整数是不是质数的时候,其它线程已经找到几千个比它大的质数,多线程执行先后的不可预见性充分地体现出来了……

找到原因了,就找解决方法吧。

SortedList造成巨大的开销,问题在于,虽然几个线程出质数是乱序的,但某个线程出质数是有序的,本来已经有序的元素,你把它全混在一起,又要付出排序的开销。

思索半天,终于想到另一个思路:为每个线程都分别一个队列(可以用System.Collections.Queue),每个线程各自把计算出的质数压到自己的队列中。然后不断比较各队列中位于第一位置的元素,把最小的、而且比当前所有线程试除整数都小的质数取出来,放到质数列数组中。明显这种算法在多线程时仍然需要更多额外开销,但这个只和线程数成正比,而不像之前的算法,开销随线程数几何增长。

调试好,一运行,看着那计算质数量的极速上升,心头是那个快感!比之前的算法快了十几倍,而且多核多线程的性能充分体现出来了!

 

C#实现多线程计算300万质数列运算时间(单位分:秒)

CPU             线程数        1    2    4
AMD 64 X2 1.6GHz(双核)   1:59 1:00 1:00
P4C 2.4GHz(单核超线程)   0:37 0:32 0:34
Core2 Q6600 2.4GHz(四核) 0:30 0:16 0:10

 

对结果基本没什么疑问。AMD的双核果然是“真双核”,双线程几乎是单线程的两倍性能。P4的超线程并没有Intel标称的厉害。Intel现在四核虽然被AMD说只是“假四核”,简单地将两个“真双核”凑在一起,但对比双核的性能提升还是令人满意。

 

AMD的整数运算性能实在不济,即使把1.6G超频到2.4G,提升50%性能后仍然落后于同频单核的P4。

 

当然这只局限于这个计算质数的应用,综合来说AMD的这款CPU还是要比P4快的。在WinRAR的性能测试中,AMD 64 X2 1.6GHz单线程下已经达到了P4 2.4C双线程的相同性能。

 

总结:我想通过这个多线程的应用说明,多线程不是简单的多个单线程,多线程并行运算是一个很复杂的技术,一个运算如果要引入多线程,你可能要多写好多倍的代码。虽然现在的高级语言的多线程支持已经很完善,但最复杂的部分仍然是和问题相关的,还须设计者通过大量努力和实践来解决。多线程下会引出大量单线程下没有的问题,并需要为这些问题付出额外的开发时间和机器开销,如果设计不当,多线程性能提升可能会不足以弥补增大的性能开销,失去了多线程的意义。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值