有关对耗时很大循环进行并行化优化的探讨之三:并行线程越多运行就会越快吗?
在.net framework4.0以后,出现了并行编程的概念,使用 Parallel.For(0, N, i =>{ ... },很容易就可以实现我们自定义的并行化循环操作,在并行循环体中,还可以操作外部的变量,这个特性是很多其他语言所没有的,当然其他语言,诸如JAVA之类,完全可以采用我们在第二篇所介绍的方法自己生成并行化操作。
由于.net framework使用环境的局限性,以及“庞大的身躯”,和安装时必须向微软“报到”的限制。很多开发商并不喜欢用,完全可以采用我所介绍的方式实现并行化,与微软的并行化的区别只是他的循环体是使用了lamda表达式,而我的方式是使用委托而已。再发散一下,我个人认为微软的并行化操作在并行化优化方面,多处理器利用方面会更有优势些,性能会更好。
但.Net FrameWork中对并行化并行化的最大线程数貌似并没有进行个性化限定,运行起来自然是并行任务能开多少开多少了。就并行线程数的问题,我们在某个项目中做了一个实验,具体的并行任务中既有数据库操作,又有通信操作,还包括了若干锁定资源操作,以下就是实验结果:
最大线程数 |
程序平均执行时间 |
单线程 |
31470 ms |
15线程 |
20042 ms |
5线程 |
20307 ms |
3线程 |
18745 ms |
2线程 |
18523 ms |
从这个有趣的实验结果可以看出,某些应用下,似乎线程数越多,执行时间反而越慢了, 很多情况下,并行化的程序性能可能反而不如顺序执行,甚至会出现一些死锁等问题,这在微软的说明中提到了很多(见http://technet.microsoft.com/zh-cn/magazine/dd997392(VS.110).aspx),从这篇文章,我们可以看到,影响性能的因素主要有两大类:
1. 对共享资源的锁定问题:这个问题比较好理解,如果多个并行任务尝试操作被锁定的内存位置,那么后来的肯定要等待,对于考虑不很周到的代码,其结果就是比串行机制还慢.
2. 循环体内的对象运行机制问题
在微软的说明中,提到了“不安全的对象”并行化的问题,比如filestream, ui对象等,
另一个有趣的操作是数据库操作及通信操作, 其特征是自身就具有不可控的性能瓶颈,最好通过优化数据库的命令,如连表查询、存储过程等处理。
但对于懒人,或者不需要再精细优化的情况下,并行任务占据的线程数越少,对整体资源及其他任务的影响也越少,所以我们可以对已经封装的并行化类进行一下最大线程数的限制:
class ParaLoop { .... private int _MaxThreadQty; private int _CurrentThreadQty; private ManualResetEvent ReqThreadEvent = new ManualResetEvent(false); private object _SynthreadQty = new object(); public ParaLoop(int mtq) { _MaxThreadQty = mtq; } private void ReleaseThread() //使用完后释放线程,并通知可以申请新线程 { lock (_SynthreadQty ) { _CurrentThreadQty--; ReqThreadEvent.Set(); } } ~ParaLoop() { ReqThreadEvent.Set(); } private MyParaThread RequestThread() // 申请线程,如果达到最大数则等待,直到有新的线程可用 { lock (_SynthreadQty) { if (_CurrentThreadQty < _MaxThreadQty) { _CurrentThreadQty++; return new MyParaThread(); } else { ReqThreadEvent.Reset(); } } ReqThreadEvent.WaitOne(); lock (_SynthreadQty) { _CurrentThreadQty++; return new MyParaThread(); } } public object ParaCompute(MyParaLoopTaskHandler loopTask, object[] inArg, int loopQty) { ... for (int i = 0; i < _TotalQty; i++) { MyParaThread u = RequestThread(); //由直接新建线程改为申请线程 // MyParaThread u = new MyParaThread(); } _ParaEvent.WaitOne(); return _ParaLoop_Return; } void u_EndTaskCallBack(bool taskRst, object retVal) { ... ReleaseThread(); //有返回值表示可以释放线程了 } } }