原文:http://blog.csdn.net/lishenglong666/article/details/8557215 最原始的博主我没有找到,只能把我从何处转的此篇博文的链接发出来。感觉这篇博文写的很棒,特此转载了
鱼还是熊掌:浅谈多进程多线程的选择
关于多进程和多线程,教科书上最经典的一句话是“进程是资源分配的最小单位,线程是CPU调度的最小单位”,这句话应付考试基本上够了,但如果在工作中遇到类似的选择问题,那就没有这么简单了,选的不好,会让你深受其害。
经常在网络上看到有的XDJM问“多进程好还是多线程好?”、“Linux下用多进程还是多线程?”等等期望一劳永逸的问题,我只能说:没有最好,只有更好。根据实际情况来判断,哪个更加合适就是哪个好。
我们按照多个不同的维度,来看看多线程和多进程的对比(注:因为是感性的比较,因此都是相对的,不是说一个好得不得了,另外一个差的无法忍受)。
对比维度 |
多进程 |
多线程 |
总结 |
数据共享、同步 |
数据共享复杂,需要用IPC;数据是分开的,同步简单 |
因为共享进程数据,数据共享简单,但也是因为这个原因导致同步复杂 |
各有优势 |
内存、CPU |
占用内存多,切换复杂,CPU利用率低 |
占用内存少,切换简单,CPU利用率高 |
线程占优 |
创建销毁、切换 |
创建销毁、切换复杂,速度慢 |
创建销毁、切换简单,速度很快 |
线程占优 |
编程、调试 |
编程简单,调试简单 |
编程复杂,调试复杂 |
进程占优 |
可靠性 |
进程间不会互相影响 |
一个线程挂掉将导致整个进程挂掉 |
进程占优 |
分布式 |
适应于多核、多机分布式;如果一台机器不够,扩展到多台机器比较简单 |
适应于多核分布式 |
进程占优 |
1)需要频繁创建销毁的优先用线程
原因请看上面的对比。
这种原则最常见的应用就是Web服务器了,来一个连接建立一个线程,断了就销毁线程,要是用进程,创建和销毁的代价是很难承受的
2)需要进行大量计算的优先使用线程
所谓大量计算,当然就是要耗费很多CPU,切换频繁了,这种情况下线程是最合适的。
这种原则最常见的是图像处理、算法处理。
3)强相关的处理用线程,弱相关的处理用进程
什么叫强相关、弱相关?理论上很难定义,给个简单的例子就明白了。
一般的Server需要完成如下任务:消息收发、消息处理。“消息收发”和“消息处理”就是弱相关的任务,而“消息处理”里面可能又分为“消息解码”、“业务处理”,这两个任务相对来说相关性就要强多了。因此“消息收发”和“消息处理”可以分进程设计,“消息解码”、“业务处理”可以分线程设计。
当然这种划分方式不是一成不变的,也可以根据实际情况进行调整。
4)可能要扩展到多机分布的用进程,多核分布的用线程
原因请看上面对比。
5)都满足需求的情况下,用你最熟悉、最拿手的方式
至于“数据共享、同步”、“编程、调试”、“可靠性”这几个维度的所谓的“复杂、简单”应该怎么取舍,我只能说:没有明确的选择方法。但我可以告诉你一个选择原则:如果多进程和多线程都能够满足要求,那么选择你最熟悉、最拿手的那个。
需要提醒的是:虽然我给了这么多的选择原则,但实际应用中基本上都是“进程+线程”的结合方式,千万不要真的陷入一种非此即彼的误区。
消耗资源:
从内核的观点看,进程的目的就是担当分配系统资源(CPU时间、内存等)的基本单位。线程是进程的一个执行流,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。
线程,它们彼此之间使用相同的地址空间,共享大部分数据,启动一个线程所花费的空间远远小于启动一个进程所花费的空间,而且,线程间彼此切换所需的时间也远远小于进程间切换所需要的时间。据统计,总的说来,一个进程的开销大约是一个线程开销的30倍左右,当然,在具体的系统上,这个数据可能会有较大的区别。
通讯方式:
进程之间传递数据只能是通过通讯的方式,即费时又不方便。线程时间数据大部分共享(线程函数内部不共享),快捷方便。但是数据同步需要锁对于static变量尤其注意
线程自身优势:
提高应用程序响应;使多CPU系统更加有效。操作系统会保证当线程数不大于CPU数目时,不同的线程运行于不同的CPU上;
改善程序结构。一个既长又复杂的进程可以考虑分为多个线程,成为几个独立或半独立的运行部分,这样的程序会利于理解和修改。
实验数据:
进程实验代码(fork.c):
进程实验代码(thread.c):
两段程序做的事情是一样的,都是创建“若干”个进程/线程,每个创建出的进程/线程打印“若干”条“hello linux”字符串到控制台和日志文件,两个“若干”由两个宏 P_NUMBER和COUNT分别定义,程序编译指令如下:
gcc -o fork fork.c
gcc -lpthread -o thread thread.c
实验通过time指令执行两个程序,抄录time输出的挂钟时间(real时间):
time ./fork
time ./thread
每批次的实验通过改动宏 P_NUMBER和COUNT来调整进程/线程数量和打印次数,每批次测试五轮,得到的结果如下:
一、重复周丽论文实验步骤
(注:本文平均值算法采用的是去掉一个最大值去掉一个最小值,然后平均)
单核(双核机器禁掉一核),进程/线程数:255,打印次数5 |
||||||
|
第1次 |
第2次 |
第3次 |
第4次 |
第5次 |
平均 |
多进程 |
0m0.070s |
0m0.071s |
0m0.071s |
0m0.070s |
0m0.070s |
0m0.070s |
多线程 |
0m0.049s |
0m0.049s |
0m0.049s |
0m0.049s |
0m0.049s |
0m0.049s |
单核(双核机器禁掉一核),进程/线程数:255,打印次数10 |
||||||
|
第1次 |
第2次 |
第3次 |
第4次 |
第5次 |
平均 |
多进程 |
0m0.112s |
0m0.101s |
0m0.100s |
0m0.085s |
0m0.121s |
0m0.104s |
多线程 |
0m0.097s |
0m0.089s |
0m0.090s |
0m0.104s |
0m0.080s |
0m0.092s |
单核(双核机器禁掉一核),进程/线程数:255,打印次数50 |
||||||
|
第1次 |
第2次 |
第3次 |
第4次 |
第5次 |
平均 |
多进程 |
0m0.459s |
0m0.531s |
0m0.499s |
0m0.499s |
0m0.524s |
0m0.507s |
多线程 |
0m0.387s |
0m0.456s |
0m0.435s |
0m0.423s |
0m0.408s |
0m0.422s |
单核(双核机器禁掉一核),进程/线程数:255,打印次数100 |
||||||
|
第1次 |
第2次 |
第3次 |
第4次 |
第5次 |
平均 |
多进程 |
0m1.141s |
0m0.992s |
0m1.134s |
0m1.027s |
0m0.965s |
0m1.051s |
多线程 |
0m0.925s |
0m0.899s |
0m0.961s |
0m0.934s |
0m0.853s |
0m0.919s |
单核(双核机器禁掉一核),进程/线程数:255,打印次数500 |
||||||
|
第1次 |
第2次 |
第3次 |
第4次 |
第5次 |
平均 |
多进程 |
0m5.221s |
0m5.258s |
0m5.706s |
0m5.288s |
0m5.455s |
0m5.334s |
多线程 |
0m4.689s |
0m4.578s |
0m4.670s |
0m4.566s |
0m4.722s |
0m4.646s |
单核(双核机器禁掉一核),进程/线程数:255,打印次数1000 |
||||||
|
第1次 |
第2次 |
第3次 |
第4次 |
第5次 |
平均 |
多进程 |
0m12.680s |
0m16.555s |
0m11.158s |
0m10.922s |
0m11.206s |
0m11.681s |
多线程 |
0m12.993s |
0m13.087s |
0m13.082s |
0m13.485s |
0m13.053s |
0m13.074s |
单核(双核机器禁掉一核),进程/线程数:255,打印次数5000 |
||||||
|
第1次 |
第2次 |
第3次 |
第4次 |
第5次 |
平均 |
多进程 |
1m27.348s |
1m5.569s |
0m57.275s |
1m5.029s |
1m15.174s |
1m8.591s |
多线程 |
1m25.813s |
1m29.299s |
1m23.842s |
1m18.914s |
1m34.872s |
1m26.318s |
单核(双核机器禁掉一核),进程/线程数:255,打印次数10000 |
||||||
|
第1次 |
第2次 |
第3次 |
第4次 |
第5次 |
平均 |
多进程 |
2m8.336s |
2m22.999s |
2m11.046s |
2m30.040s |
2m5.752s |
2m14.137s |
多线程 |
2m46.666s |
2m44.757s |
2m34.528s |
2m15.018s |
2m41.436s |
2m40.240s |
、增加每进程/线程的工作强度的实验
这次将程序打印数据增大,原来打印字符串为:
现在修改为每次打印256个字节数据:
单核(双核机器禁掉一核),进程/线程数:255 ,打印次数100 |
||||||
|
第1次 |
第2次 |
第3次 |
第4次 |
第5次 |
平均 |
多进程 |
0m6.977s |
0m7.358s |
0m7.520s |
0m7.282s |
0m7.218s |
0m7.286 |
多线程 |
0m7.035s |
0m7.552s |
0m7.087s |
0m7.427s |
0m7.257s |
0m7.257 |
单核(双核机器禁掉一核),进程/线程数: 255,打印次数500 |
||||||
|
第1次 |
第2次 |
第3次 |
第4次 |
第5次 |
平均 |
多进程 |
0m35.666s |
0m36.009s |
0m36.532s |
0m35.578s |
0m41.537s |
0m36.069 |
多线程 |
0m37.290s |
0m35.688s |
0m36.377s |
0m36.693s |
0m36.784s |
0m36.618 |
单核(双核机器禁掉一核),进程/线程数: 255,打印次数1000 |
||||||
|
第1次 |
第2次 |
第3次 |
第4次 |
第5次 |
平均 |
多进程 |
1m8.864s |
1m11.056s |
1m10.273s |
1m12.317s |
1m20.193s |
1m11.215 |
多线程 |
1m11.949s |
1m13.088s |
1m12.291s |
1m9.677s |
1m12.210s |
1m12.150 |
【实验结论】
从上面的实验比对结果看,即使Linux2.6使用了新的NPTL线程库(据说比原线程库性能提高了很多,唉,又是据说!),多线程比较多进程在效率上没有任何的优势,在线程数增大时多线程程序还出现了运行错误,实验可以得出下面的结论:
在Linux2.6上,多线程并不比多进程速度快,考虑到线程栈的问题,多进程在并发上有优势。
四、多进程和多线程在创建和销毁上的效率比较
预先创建进程或线程可以节省进程或线程的创建、销毁时间,在实际的应用中很多程序使用了这样的策略,比如Apapche预先创建进程、Tomcat 预先创建线程,通常叫做进程池或线程池。在大部分人的概念中,进程或线程的创建、销毁是比较耗时的,在stevesn的著作《Unix网络编程》中有这样 的对比图(第一卷 第三版 30章 客户/服务器程序设计范式):
行号 | 服务器描述 | 进程控制CPU时间(秒,与基准之差) | ||
---|---|---|---|---|
Solaris2.5.1 | Digital Unix4.0b | BSD/OS3.0 | ||
0 | 迭代服务器(基准测试,无进程控制) | 0.0 | 0.0 | 0.0 |
1 | 简单并发服务,为每个客户请求fork一个进程 | 504.2 | 168.9 | 29.6 |
2 | 预先派生子进程,每个子进程调用accept | 6.2 | 1.8 | |
3 | 预先派生子进程,用文件锁保护accept | 25.2 | 10.0 | 2.7 |
4 | 预先派生子进程,用线程互斥锁保护accept | 21.5 | ||
5 | 预先派生子进程,由父进程向子进程传递套接字 | 36.7 | 10.9 | 6.1 |
6 | 并发服务,为每个客户请求创建一个线程 | 18.7 | 4.7 | |
7 | 预先创建线程,用互斥锁保护accept | 8.6 | 3.5 | |
8 | 预先创建线程,由主线程调用accept | 14.5 | 5.0 |
stevens已驾鹤西去多年,但《Unix网络编程》一书仍具有巨大的影响力,上表中stevens比较了三种服务器上多进程和多线程的执行效 率,因为三种服务器所用计算机不同,表中数据只能纵向比较,而横向无可比性,stevens在书中提供了这些测试程序的源码(也可以在网上下载)。书中介 绍了测试环境,两台与服务器处于同一子网的客户机,每个客户并发5个进程(服务器同一时间最多10个连接),每个客户请求从服务器获取4000字节数据, 预先派生子进程或线程的数量是15个。
第0行是迭代模式的基准测试程序,服务器程序只有一个进程在运行(同一时间只能处理一个客户请求),因为没有进程或线程的调度切换,因此它的速度是 最快的,表中其他服务模式的运行数值是比迭代模式多出的差值。迭代模式很少用到,在现有的互联网服务中,DNS、NTP服务有它的影子。第1~5行是多进 程服务模式,期中第1行使用现场fork子进程,2~5行都是预先创建15个子进程模式,在多进程程序中套接字传递不太容易(相对于多线 程),stevens在这里提供了4个不同的处理accept的方法。6~8行是多线程服务模式,第6行是现场为客户请求创建子线程,7~8行是预先创建 15个线程。表中有的格子是空白的,是因为这个系统不支持此种模式,比如当年的BSD不支持线程,因此BSD上多线程的数据都是空白的。
从数据的比对看,现场为每客户fork一个进程的方式是最慢的,差不多有20倍的速度差异,Solaris上的现场fork和预先创建子进程的最大差别是504.2 :21.5,但我们不能理解为预先创建模式比现场fork快20倍,原因有两个:
1. stevens的测试已是十几年前的了,现在的OS和CPU已起了翻天覆地的变化,表中的数值需要重新测试。
2. stevens没有提供服务器程序整体的运行计时,我们无法理解504.2 :21.5的实际运行效率,有可能是1504.2 : 1021.5,也可能是100503.2 : 100021.5,20倍的差异可能很大,也可能可以忽略。
因此我写了下面的实验程序,来计算在Linux2.6上创建、销毁10万个进程/线程的绝对用时。
创建10万个进程(forkcreat.c):
创建10万个线程(pthreadcreat.c):
创建10万个线程的Java程序:
单核(双核机器禁掉一核),创建销毁10万个进程/线程 |
||||||
|
第1次 |
第2次 |
第3次 |
第4次 |
第5次 |
平均 |
多进程 |
0m8.774s |
0m8.780s |
0m8.475s |
0m8.592s |
0m8.687s |
0m8.684 |
多线程 |
0m0.663s |
0m0.660s |
0m0.662s |
0m0.660s |
0m0.661s |
0m0.661 |
创建销毁10万个线程(Java) |
---|
12286毫秒 |
从数据可以看出,多线程比多进程在效率上有10多倍的优势,但不能让我们在使用哪种并发模式上定性,这让我想起多年前政治课上的一个场景:在讲到优越性时,面对着几个对此发表质疑评论的调皮男生,我们的政治老师发表了高见,“不能只横向地和当今的发达国家比,你应该纵向地和过去中国几十年的发展历史 比”。政治老师的话套用在当前简直就是真理,我们看看,即使是在赛扬CPU上,创建、销毁进程/线程的速度都是空前的,可以说是有质的飞跃的,平均创建销毁一个进程的速度是0.18毫秒,对于当前服务器几百、几千的并发量,还有预先派生子进程/线程的必要吗?
预先派生子进程/线程比现场创建子进程/线程要复杂很多,不仅要对池中进程/线程数量进行动态管理,还要解决多进程/多线程对accept的“抢” 问题,在stevens的测试程序中,使用了“惊群”和“锁”技术。即使stevens的数据表格中,预先派生线程也不见得比现场创建线程快,在 《Unix网络编程》第三版中,新作者参照stevens的测试也提供了一组数据,在这组数据中,现场创建线程模式比预先派生线程模式已有了效率上的优势。因此我对这一节实验下的结论是:
预先派生进程/线程的模式(进程池、线程池)技术,不仅复杂,在效率上也无优势,在新的应用中可以放心大胆地为客户连接请求去现场创建进程和线程。
我想,这是fork迷们最愿意看到的结论了。
五、双核系统重复周丽论文实验步骤
双核,进程/线程数:255 ,打印次数10 |
||||||
|
第1次 |
第2次 |
第3次 |
第4次 |
第5次 |
平均(单核倍数) |
多进程 |
0m0.061s |
0m0.053s |
0m0.068s |
0m0.061s |
0m0.059s |
0m0.060(1.73) |
多线程 |
0m0.054s |
0m0.040s |
0m0.053s |
0m0.056s |
0m0.042s |
0m0.050(1.84) |
双核,进程/线程数: 255,打印次数100 |
||||||
|
第1次 |
第2次 |
第3次 |
第4次 |
第5次 |
平均(单核倍数) |
多进程 |
0m0.918s |
0m1.198s |
0m1.241s |
0m1.017s |
0m1.172s |
0m1.129(0.93) |
多线程 |
0m0.897s |
0m1.166s |
0m1.091s |
0m1.360s |
0m0.997s |
0m1.085(0.85) |
双核,进程/线程数: 255,打印次数1000 |
||||||
|
第1次 |
第2次 |
第3次 |
第4次 |
第5次 |
平均(单核倍数) |
多进程 |
0m11.276s |
0m11.269s |
0m11.218s |
0m10.919s |
0m11.201s |
0m11.229(1.04) |
多线程 |
0m11.525s |
0m11.984s |
0m11.715s |
0m11.433s |
0m10.966s |
0m11.558(1.13) |
双核,进程/线程数:255 ,打印次数10000 |
||||||
|
第1次 |
第2次 |
第3次 |
第4次 |
第5次 |
平均(单核倍数) |
多进程 |
1m54.328s |
1m54.748s |
1m54.807s |
1m55.950s |
1m57.655s |
1m55.168(1.16) |
多线程 |
2m3.021s |
1m57.611s |
1m59.139s |
1m58.297s |
1m57.258s |
1m58.349(1.35) |
【实验结论】
双核处理器在完成任务量较少时,没有系统其他瓶颈因素影响时基本上是单核的两倍,在任务量较多时,受系统其他瓶颈因素的影响,速度明显趋近于单核的速度。
六、并发服务的不可测性
看到这里,你会感觉到我有挺进程、贬线程的论调,实际上对于现实中的并发服务具有不可测性,前面的实验和结论只可做参考,而不可定性。对于不可测性,我举个生活中的例子。
这几年在大都市生活的朋友都感觉城市交通状况越来越差,到处堵车,从好的方面想这不正反应了我国GDP的高速发展。如果你7、8年前来到西安市,穿 过南二环上的一些十字路口时,会发现一个奇怪的U型弯的交通管制,为了更好的说明,我画了两张图来说明,第一张图是采用U型弯之前的,第二张是采用U型弯 之后的。
南二环交通图一
南二环交通图二
为了讲述的方便,我们不考虑十字路口左拐的情况,在图一中东西向和南北向的车辆交汇在十字路口,用红绿灯控制同一时间只能东西向或南北向通行,一般 的十字路口都是这样管控的。随着车辆的增多,十字路口的堵塞越来越严重,尤其是上下班时间经常出现堵死现象。于是交通部门在不动用过多经费的情况下而采用 了图二的交通管制,东西向车辆行进方式不变,而南北向车辆不能直行,需要右拐到下一个路口拐一个超大的U型弯,这样的措施避免了因车辆交错而引发堵死的次 数,从而提高了车辆的通过效率。我曾经问一个每天上下班乘公交经过此路口的同事,他说这样的改动不一定每次上下班时间都能缩短,但上班时间有保障了,从而 迟到次数减少了。如果今天你去西安市的南二环已经见不到U型弯了,东西向建设了高架桥,车辆分流后下层的十字路口已恢复为图一方式。
从效率的角度分析,在图一中等一个红灯45秒,远远小于图二拐那个U型弯用去的时间,但实际情况正好相反。我们可以设想一下,如果路上的所有运行车 辆都是同一型号(比如说全是QQ3微型车),所有的司机都遵守交规,具有同样的心情和性格,那么图一的通行效率肯定比图二高。现实中就不一样了,首先车辆 不统一,有大车、小车、快车、慢车,其次司机的品行不一,有特别遵守交规的,有想耍点小聪明的,有性子慢的,也有的性子急,时不时还有三轮摩托逆行一下, 十字路口的“死锁”也就难免了。
那么在什么情况下图二优于图一,是否能拿出一个科学分析数据来呢?以现在的科学技术水平是拿不出来的,就像长期的天气预报不可预测一样,西安市的交管部门肯定不是分析各种车辆的运行规律、速度,再进行复杂的社会学、心理学分析做出U型弯的决定的,这就是要说的不可测性。
现实中的程序亦然如此,比如WEB服务器,有的客户在快车道(宽带),有的在慢车道(窄带),有的性子慢(等待半分钟也无所谓),有的性子急(拼命 的进行浏览器刷新),时不时还有一两个黑客混入其中,这种情况每个服务器都不一样,既是是同一服务器每时每刻的变化也不一样,因此说不具有可测性。开发者 和维护者能做的,不论是前面的这种实验测试,还是对具体网站进行的压力测试,最多也就能模拟相当于QQ3通过十字路口的场景。
结束语
本篇文章比较了Linux系统上多线程和多进程的运行效率,在实际应用时还有其他因素的影响,比如网络通讯时采用长连接还是短连接,是否采用 select、poll,java中称为nio的机制,还有使用的编程语言,例如Java不能使用多进程,PHP不能使用多线程,这些都可能影响到并发模 式的选型。
最后还有两点提醒:
1. 文章中的所有实验数据有环境约束。
2. 由于并行服务的不可测性,文章中的观点应该只做参考,而不要去定性。