多核 多线程 进程的概念

今天吃饭的时候,聊起了一个困扰我好久的问题。查了些资料加上本身的一些理解,若是不对,请指正:html

咱们在买电脑的时候常常遇到一些概念,我这电脑是多核多线程的,什么双核的,什么四核、八核的,这种运动速度电脑快!那么这样的电脑为何运行速度快?固然,运行速度快有不少缘由,好比主频、缓存什么的。这里咱们只说,java

为何多核会致使运行速度快?至于多线程为何会致使运行速度快,有一篇里面我介绍了。linux

从内核的观点看,进程的目的就是担当分配系统资源(cpu时间。内存等)的基本单位。git

线程是进程的一个执行流,是CPU调度和分派的基本单位,他是比进程更小的能独立运行的基本单位。程序员

 多内核(multicore chips)是指在一枚处理器(chip)中集成两个或多个完整的计算引擎(内核)。这是百度百度的定义,其实说白了, 多核就是多个CPU,能够实现并行操做!



下面给出一个参考连接,该连接并无提升多核的概念,只是对比了对线程和多进程,不过感受写的很专业。web

参考连接:http://blog.csdn.net/lishenglong666/article/details/8557215算法

关于多进程和多线程,教科书上最经典的一句话是“进程是资源分配的最小单位,线程是CPU调度的最小单位”,这句话应付考试基本上够了,但若是在工做中遇到相似的选择问题,那就没有这么简单了,选的很差,会让你深受其害。编程

 

常常在网络上看到有的XDJM问“多进程好仍是多线程好?”、“Linux下用多进程仍是多线程?”等等指望一劳永逸的问题,我只能说:没有最好,只有更好。根据实际状况来判断,哪一个更加合适就是哪一个好。数组

 

咱们按照多个不一样的维度,来看看多线程和多进程的对比(注:由于是感性的比较,所以都是相对的,不是说一个好得不得了,另一个差的没法忍受)。缓存

对比维度

多进程

多线程

总结

数据共享、同步

数据共享复杂,须要用IPC;数据是分开的,同步简单

由于共享进程数据,数据共享简单,但也是由于这个缘由致使同步复杂

各有优点

内存、CPU

占用内存多,切换复杂,CPU利用率低

占用内存少,切换简单,CPU利用率高

线程占优

建立销毁、切换

建立销毁、切换复杂,速度慢

建立销毁、切换简单,速度很快

线程占优

编程、调试

编程简单,调试简单

编程复杂,调试复杂

进程占优

可靠性

进程间不会互相影响

一个线程挂掉将致使整个进程挂掉

进程占优

分布式

适应于多核、多机分布式;若是一台机器不够,扩展到多台机器比较简单

适应于多核分布式

进程占优

 

看起来比较简单,优点对比上是“线程 3.5 v 2.5 进程”,咱们只管选线程就是了?

 

呵呵,有这么简单我就不用在这里浪费口舌了,仍是那句话,没有绝对的好与坏,只有哪一个更加合适的问题。咱们来看实际应用中究竟如何判断更加合适。

1)须要频繁建立销毁的优先用线程

缘由请看上面的对比。

这种原则最多见的应用就是Web服务器了,来一个链接创建一个线程,断了就销毁线程,要是用进程,建立和销毁的代价是很难承受的

2)须要进行大量计算的优先使用线程

所谓大量计算,固然就是要耗费不少CPU,切换频繁了,这种状况下线程是最合适的。

这种原则最多见的是图像处理、算法处理。

3)强相关的处理用线程,弱相关的处理用进程

什么叫强相关、弱相关?理论上很难定义,给个简单的例子就明白了。

通常的Server须要完成以下任务:消息收发、消息处理。“消息收发”和“消息处理”就是弱相关的任务,而“消息处理”里面可能又分为“消息解码”、“业务处理”,这两个任务相对来讲相关性就要强多了。所以“消息收发”和“消息处理”能够分进程设计,“消息解码”、“业务处理”能够分线程设计。

固然这种划分方式不是一成不变的,也能够根据实际状况进行调整。

4)可能要扩展到多机分布的用进程,多核分布的用线程

缘由请看上面对比。

5)都知足需求的状况下,用你最熟悉、最拿手的方式

至于“数据共享、同步”、“编程、调试”、“可靠性”这几个维度的所谓的“复杂、简单”应该怎么取舍,我只能说:没有明确的选择方法。但我能够告诉你一个选择原则:若是多进程和多线程都可以知足要求,那么选择你最熟悉、最拿手的那个。

 

须要提醒的是:虽然我给了这么多的选择原则,但实际应用中基本上都是“进程+线程”的结合方式,千万不要真的陷入一种非此即彼的误区。


一、进程与线程

进程是程序执行时的一个实例,即它是程序已经执行到课中程度的数据结构的聚集。从内核的观点看,进程的目的就是担当分配系统资源(CPU时间、内存等)的基本单位

线程是进程的一个执行流,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。一个进程由几个线程组成(拥有不少相对独立的执行流的用户程序共享应用程序的大部分数据结构),线程与同属一个进程的其余的线程共享进程所拥有的所有资源。

"进程——资源分配的最小单位,线程——程序执行的最小单位"

进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不一样执行路径。线程有本身的堆栈和局部变量,但线程没有单独的地址空间,一个线程死掉就等于整个进程死掉,因此多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行而且又要共享某些变量的并发操做,只能用线程,不能用进程。

 

总的来讲就是:进程有独立的地址空间,线程没有单独的地址空间(同一进程内的线程共享进程的地址空间)。(下面的内容摘自Linux下的多线程编程

使用多线程的理由之一是和进程相比,它是一种很是"节俭"的多任务操做方式。咱们知道,在Linux系统下,启动一个新的进程必须分配给它独立的地址空间,创建众多的数据表来维护它的代码段、堆栈段和数据段,这是一种"昂贵"的多任务工做方式。而运行于一个进程中的多个线程,它们彼此之间使用相同的地址空间,共享大部分数据,启动一个线程所花费的空间远远小于启动一个进程所花费的空间,并且,线程间彼此切换所需的时间也远远小于进程间切换所须要的时间。据统计,总的说来,一个进程的开销大约是一个线程开销的30倍左右,固然,在具体的系统上,这个数据可能会有较大的区别。

使用多线程的理由之二是线程间方便的通讯机制。对不一样进程来讲,它们具备独立的数据空间,要进行数据的传递只能经过通讯的方式进行,这种方式不只费时,并且很不方便。线程则否则,因为同一进程下的线程之间共享数据空间,因此一个线程的数据能够直接为其它线程所用,这不只快捷,并且方便。固然,数据的共享也带来其余一些问题,有的变量不能同时被两个线程所修改,有的子程序中声明为static的数据更有可能给多线程程序带来灾难性的打击,这些正是编写多线程程序时最须要注意的地方。

除了以上所说的优势外,不和进程比较,多线程程序做为一种多任务、并发的工做方式,固然有如下的优势:

  • 提升应用程序响应。这对图形界面的程序尤为有意义,当一个操做耗时很长时,整个系统都会等待这个操做,此时程序不会响应键盘、鼠标、菜单的操做,而使用多线程技术,将耗时长的操做(time consuming)置于一个新的线程,能够避免这种尴尬的状况。
  • 使多CPU系统更加有效。操做系统会保证当线程数不大于CPU数目时,不一样的线程运行于不一样的CPU上。
  • 改善程序结构。一个既长又复杂的进程能够考虑分为多个线程,成为几个独立或半独立的运行部分,这样的程序会利于理解和修改。


在Unix上编程采用多线程仍是多进程的争执由来已久,这种争执最多见到在B/S通信中服务端并发技术 的选型上,好比WEB服务器技术中,Apache是采用多进程的(perfork模式,每客户链接对应一个进程,每进程中只存在惟一一个执行线 程),Java的Web容器Tomcat、Websphere等都是多线程的(每客户链接对应一个线程,全部线程都在一个进程中)。

从Unix发展历史看,伴随着Unix的诞生多进程就出现了,而多线程很晚才被系统支持,例如Linux直到内核2.6,才支持符合Posix规范的NPTL线程库。进程和线程的特色,也就是各自的优缺点以下:

进程优势:编程、调试简单,可靠性较高。
进程缺点:建立、销毁、切换速度慢,内存、资源占用大。
线程优势:建立、销毁、切换速度快,内存、资源占用小。
线程缺点:编程、调试复杂,可靠性较差。

上面的对比能够归结为一句话:“线程快而进程可靠性高”。线程有个别名叫“轻量级进程”,在有的书籍资料上介绍线程能够十倍、百倍的效率快于进程; 而进程之间不共享数据,没有锁问题,结构简单,一个进程崩溃不像线程那样影响全局,所以比较可靠。我相信这个观点能够被大部分人所接受,由于和咱们所接受的知识概念是相符的。

在写这篇文章前,我也属于这“大部分人”,这两年在用C语言编写的几个C/S通信程序中,因时间紧老是采用多进程并发技术,并且是比较简单的现场为 每客户fork()一个进程,当时老是担忧并发量增大时负荷可否承受,盘算着等时间充裕了将它改成多线程形式,或者改成预先建立进程的形式,直到最近在网 上看到了一篇论文《Linux系统下多线程与多进程性能分析》做者“周丽 焦程波 兰巨龙”,才认真思考这个问题,我本身也作了实验,结论和论文做者的类似,但对大部分人能够说是颠覆性的。

下面是得出结论的实验步骤和过程,结论到底是怎样的? 感兴趣就一块儿看看吧。

实验代码使用周丽论文中的代码样例,我作了少许修改,值得注意的是这样的区别:

论文实验和个人实验时间不一样,论文所处的年代linux内核是2.4,个人实验linux内核是2.6,2.6使用的线程库是NPTL,2.4使用的是老的Linux线程库(用进程模拟线程的那个LinuxThread)。

论文实验和我用的机器不一样,论文描述了使用的环境:单cpu 机器基本配置为:celeron 2.0 GZ, 256M, Linux 9.2,内核 2.4.8。个人环境是:双核 Intel(R) Xeon(R) CPU 5130  @ 2.00GHz(作实验时,禁掉了一核),512MG内存,Red Hat Enterprise Linux ES release 4 (Nahant Update 4),内核2.6.9-42。

进程实验代码(fork.c):

  1. #include <stdlib.h>
  2. #include <stdio.h>
  3. #include <signal.h>

  4. #define P_NUMBER 255 //并发进程数量
  5. #define COUNT 5 //每次进程打印字符串数
  6. #define TEST_LOGFILE "logFile.log"
  7. FILE *logFile=NULL;

  8. char *s="hello linux\0";

  9. int main()
  10. {
  11.     int i=0,j=0;
  12.     logFile=fopen(TEST_LOGFILE,"a+");//打开日志文件
  13.     for(i=0;i<P_NUMBER;i++)
  14.     {
  15.         if(fork()==0)//建立子进程,if(fork()==0){}这段代码是子进程运行区间
  16.         {
  17.             for(j=0;j<COUNT;j++)
  18.             {
  19.                 printf("[%d]%s\n",j,s);//向控制台输出
  20.                 /*当你频繁读写文件的时候,Linux内核为了提升读写性能与速度,会将文件在内存中进行缓存,这部份内存就是Cache Memory(缓存内存)。可能致使测试结果不许,因此在此注释*/
  21.                 //fprintf(logFile,"[%d]%s\n",j,s);//向日志文件输出,
  22.             }
  23.             exit(0);//子进程结束
  24.         }
  25.     }
  26.     
  27.     for(i=0;i<P_NUMBER;i++)//回收子进程
  28.     {
  29.         wait(0);
  30.     }
  31.     
  32.     printf("Okay\n");
  33.     return 0;
  34. }

进程实验代码(thread.c):

  1. #include <pthread.h>
  2. #include <unistd.h>
  3. #include <stdlib.h>
  4. #include <stdio.h>

  5. #define P_NUMBER 255//并发线程数量
  6. #define COUNT 5 //每线程打印字符串数
  7. #define TEST_LOG "logFile.log"
  8. FILE *logFile=NULL;

  9. char *s="hello linux\0";

  10. print_hello_linux()//线程执行的函数
  11. {
  12.     int i=0;
  13.     for(i=0;i<COUNT;i++)
  14.     {
  15.         printf("[%d]%s\n",i,s);//想控制台输出
  16.         /*当你频繁读写文件的时候,Linux内核为了提升读写性能与速度,会将文件在内存中进行缓存,这部份内存就是Cache Memory(缓存内存)。可能致使测试结果不许,因此在此注释*/
  17.         //fprintf(logFile,"[%d]%s\n",i,s);//向日志文件输出
  18.     }
  19.     pthread_exit(0);//线程结束
  20. }

  21. int main()
  22. {
  23.     int i=0;
  24.     pthread_t pid[P_NUMBER];//线程数组
  25.     logFile=fopen(TEST_LOG,"a+");//打开日志文件
  26.     
  27.     for(i=0;i<P_NUMBER;i++)
  28.         pthread_create(&pid[i],NULL,(void *)print_hello_linux,NULL);//建立线程
  29.         
  30.     for(i=0;i<P_NUMBER;i++)
  31.         pthread_join(pid[i],NULL);//回收线程
  32.         
  33.     printf("Okay\n");
  34.     return 0;
  35. }

两段程序作的事情是同样的,都是建立“若干”个进程/线程,每一个建立出的进程/线程打印“若干”条“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来调整进程/线程数量和打印次数,每批次测试五轮,获得的结果以下:

1、重复周丽论文实验步骤

(注:本文平均值算法采用的是去掉一个最大值去掉一个最小值,而后平均)

单核(双核机器禁掉一核),进程/线程数: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 


本轮实验是为了和周丽论文做对比,所以将进程/线程数量限制在255个,论文也是测试了255个进程/线程分别进行5次,10 次,50 次,100 次,500 次……10000 次打印的用时,论文得出的结果是:任务量较大时,多进程比多线程效率高;而完成的任务量较小时,多线程比多进程要快,重复打印 600 次时,多进程与多线程所耗费的时间相同。

虽然个人实验直到1000打印次数时,多进程才开始领先,但考虑到使用的是NPTL线程库的缘故,从而能够证明了论文的观点。从个人实验数据看,多线程和多进程两组数据很是接近,考虑到数据的提取具备瞬间性,所以能够认为他们的速度是相同的。

是否是能够得出这样的结论:多线程建立、销毁速度快,而多线程切换速度快,这个结论咱们会在第二个试验中继续试图验证

当前的网络环境中,咱们更看中高并发、高负荷下的性能,纵观前面的实验步骤,最长的实验周期不过2分钟多一点,所以下面的实验将向两个方向延伸,第一,增长并发数量,第二,增长每进程/线程的工做强度。

2、增长并发数量的实验

下面的实验打印次数不变,而进程/线程数量逐渐增长。在实验过程当中多线程程序在后四组(线程数350,500,800,1000)的测试中都出现了“段错误”,出现错误的缘由和多线程预分配线程栈有关。

实验中的计算机CPU是32位,寻址最大范围是4GB(2的32次方),Linux是按照3GB/1GB的方式来分配内存,其中1GB属于全部进程共享的内核空间,3GB属于用户空间(进程虚拟内存空间)。Linux2.6的默认线程栈大小是8M(经过ulimit -a查看),对于多线程,在建立线程的时候系统会为每个线程预分配线程栈地址空间,也就是8M的虚拟内存空间。线程数量太多时,线程栈累计的大小将超过进程虚拟内存空间大小(计算时须要排除程序文本、数据、共享库等占用的空间),这就是实验中出现的“段错误”的缘由。

Linux2.6的默认线程栈大小能够经过 ulimit -s 命令查看或修改,咱们能够计算出线程数的最大上线: (1024*1024*1024*3) / (1024*1024*8) = 384,实际数字应该略小与384,由于还要计算程序文本、数据、共享库等占用的空间。在当今的稍显繁忙的WEB服务器上,突破384的并发访问并非稀 罕的事情,要继续下面的实验须要将默认线程栈的大小减少,但这样作有必定的风险,好比线程中的函数分配了大量的自动变量或者函数涉及很深的栈帧(典型的是 递归调用),线程栈就可能不够用了。能够配合使用POSIX.1规定的两个线程属性guardsize和stackaddr来解决线程栈溢出问 题,guardsize控制着线程栈末尾以后的一篇内存区域,一旦线程栈在使用中溢出并到达了这片内存,程序能够捕获系统内核发出的告警信号,而后使用 malloc获取另外的内存,并经过stackaddr改变线程栈的位置,以得到额外的栈空间,这个动态扩展栈空间办法须要手工编程,并且很是麻烦。

有两种方法能够改变线程栈的大小,使用 ulimit -s 命令改变系统默认线程栈的大小,或者在代码中建立线程时经过pthread_attr_setstacksize函数改变栈尺寸,在实验中使用的是第一种,在程序运行前先执行ulimit指令将默认线程栈大小改成1M:

ulimit -s 1024
time ./thread

单核(双核机器禁掉一核),进程/线程数:100 ,打印次数1000

 

1

2

3

4

5

平均

多进程

 0m3.834s

 0m3.759s

 0m4.376s

 0m3.936s

 0m3.851s

 0m3.874

多线程

 0m3.646s

0m4.498s

 0m4.219s

 0m3.893s

 0m3.943s

 0m4.018

单核(双核机器禁掉一核),进程/线程数:255 ,打印次数1000

 

1

2

3

4

5

平均

多进程

 0m9.731s

 0m9.833s

 0m10.046s

 0m9.830s

 0m9.866s

 0m9.843s

多线程

 0m9.961s

 0m9.699s

 0m9.958s

 0m10.111s

 0m9.686s

 0m9.873s


单核(双核机器禁掉一核),进程/线程数:350  ,打印次数1000

 

1

2

3

4

5

平均

多进程

 0m13.773s

 0m13.500s

 0m13.519s

 0m13.474s

 0m13.351s

 0m13.498

多线程

 0m12.754s

0m13.251s 

 0m12.813s

 0m16.861s

 0m12.764s

 0m12.943


单核(双核机器禁掉一核),进程/线程数: 500 ,打印次数1000

 

1

2

3

4

5

平均

多进程

 0m23.762s

 0m22.151s

 0m23.926s

 0m21.327s

 0m21.429s

 0m22.413

多线程

 0m20.603s

 0m20.291s

 0m21.654s

 0m20.684s

 0m20.671s

 0m20.653


单核(双核机器禁掉一核),进程/线程数:800  ,打印次数1000

 

1

2

3

4

5

平均

多进程

 0m33.616s

 0m31.757s

 0m31.759s

 0m32.232s

 0m32.498s

 0m32.163

多线程

 0m32.050s

 0m32.787s

 0m33.055s

 0m32.902s

 0m32.235s

 0m32.641


单核(双核机器禁掉一核),进程/线程数: 1000 ,打印次数1000

 

1

2

3

4

5

平均

多进程

 0m40.301s

 0m41.083s

 0m41.634s

 0m40.247s

 0m40.717s

 0m40.700

多线程

 0m41.633s

 0m41.118s

 0m42.700s

 0m42.134s

 0m41.170s

 0m41.646


【实验结论】 
当线程/进程逐渐增多时,执行相同任务时,线程所花费时间相对于进程有降低的趋势(本人怀疑后两组数据受系统其余瓶颈的影响),这是否是进一步验证了 多线程建立、销毁速度快,而多进程切换速度快。

出现了线程栈的问题,让我特别关心Java线程是怎样处理的,所以用Java语言写了一样的实验程序,Java程序加载虚拟机环境比较耗时,因此没 有用time提取测试时间,而直接将测时写入代码。对Linux上的C编程不熟悉的Java程序员也能够用这个程序去对比理解上面的C语言试验程序。
  1. import java.io.File;
  2.     import java.io.FileNotFoundException;
  3.     import java.io.FileOutputStream;
  4.     import java.io.IOException;
  5.      
  6.     public class MyThread extends Thread
  7.     {
  8.         static int P_NUMBER = 1000; /* 并发线程数量 */
  9.         static int COUNT = 1000; /* 每线程打印字符串次数 */
  10.      
  11.         static String s = "hello linux\n";
  12.            
  13.         static FileOutputStream out = null; /* 文件输出流 */
  14.         @Override
  15.         public void run()
  16.         {
  17.             for (int i = 0; i < COUNT; i++)
  18.             {
  19.                 System.out.printf("[%d]%s", i, s); /* 向控制台输出 */
  20.                
  21.                 StringBuilder sb = new StringBuilder(16);
  22.                 sb.append("[").append(i).append("]").append(s);
  23.                 try
  24.                 {
  25.                     out.write(sb.toString().getBytes());/* 向日志文件输出 */
  26.                 }
  27.                 catch (IOException e)
  28.                 {
  29.                     e.printStackTrace();
  30.                 }
  31.             }
  32.         }
  33.      
  34.         public static void main(String[] args) throws FileNotFoundException, InterruptedException
  35.         {
  36.             MyThread[] threads = new MyThread[P_NUMBER]; /* 线程数组 */
  37.            
  38.             File file = new File("Javalogfile.log");
  39.             out = new FileOutputStream(file, true); /* 日志文件输出流 */
  40.            
  41.             System.out.println("开始运行");
  42.             long start = System.currentTimeMillis();
  43.      
  44.             for (int i = 0; i < P_NUMBER; i++) //建立线程
  45.             {
  46.                 threads[i] = new MyThread();
  47.                 threads[i].start();
  48.             }
  49.      
  50.             for (int i = 0; i < P_NUMBER; i++) //回收线程
  51.             {
  52.                 threads[i].join();
  53.             }
  54.            
  55.             System.out.println("用时:" + (System.currentTimeMillis() – start) + " 毫秒");
  56.             return;
  57.         }
  58.       
  59.     }

进程/线程数:1000  ,打印次数1000(用得原做者的数据)

 

1

2

3

4

5

平均

多线程

 65664 ms

 66269 ms

 65546ms

 65931ms

 66540ms

 65990 ms

Java程序比C程序慢一些在情理之中,但Java程序并无出现线程栈问题,5次测试都平稳完成,能够用下面的ps指令得到java进程中线程的数量:

diaoyf@ali:~$ ps -eLf | grep MyThread | wc -l
1010

用ps测试线程数在1010上维持了很长时间,多出的10个线程应该是jvm内部的管理线程,好比用于GC。我不知道Java建立线程时默认栈的大 小是多少,不少资料说法不统一,因而下载了Java的源码jdk-6u21-fcs-src-b07-jrl-17_jul_2010.jar(实验环境 安装的是 SUN jdk 1.6.0_20-b02),但没能从中找到须要的信息。对于jvm的运行,java提供了控制参数,所以再次测试时,经过下面的参数将Java线程栈大 小定义在8192k,和Linux的默认大小一致:

diaoyf@ali:~/tmp1$ java -Xss8192k MyThread

出乎意料的是并无出现想象中的异常,但用ps侦测线程数最高到达337,我判断程序在建立线程时在栈到达可用内存的上线时就中止继续建立了,程序运行的时间远小于估计值也证实了这个判断。程序虽然没有抛出异常,但运行的并不正常,另外一个问题是最后并无打印出“用时 xxx毫秒”信息。

此次测试更加深了个人一个长期的猜想:Java的Web容器不稳定。由于我是多年编写B/S的Java程序员,WEB服务不稳定经常挂掉也是司空见惯的,除了本身或项目组成员水平不高,代码编写太烂的缘由以外,我一直猜想还有更深层的缘由,若是就是线程缘由的话,这颠覆性可比本篇文章的多进程性能颠覆性要大得多,想一想世界上有多少Tomcat、Jboss、Websphere、weblogic在跑着,嘿嘿。

此次测试还打破了之前的一个说法:单CPU上并发超过六、7百,线程或进程间的切换就会占用大量CPU时间,形成服务器效率会急剧降低。但从上面的实验来看,进程/线程数到1000时(这差很少是很是繁忙的WEB服务器了),仍具备很好的线性。

3、增长每进程/线程的工做强度的实验

此次将程序打印数据增大,原来打印字符串为:

  1. char *s = "hello linux\0";

如今修改成每次打印256个字节数据:

  1. char *= "1234567890abcdef\
  2.     1234567890abcdef\
  3.     1234567890abcdef\
  4.     1234567890abcdef\
  5.     1234567890abcdef\
  6.     1234567890abcdef\
  7.     1234567890abcdef\
  8.     1234567890abcdef\
  9.     1234567890abcdef\
  10.     1234567890abcdef\
  11.     1234567890abcdef\
  12.     1234567890abcdef\
  13.     1234567890abcdef\
  14.     1234567890abcdef\
  15.     1234567890abcdef\
  16.     1234567890abcdef\0";

单核(双核机器禁掉一核),进程/线程数: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上,多线程并不比多进程速度快,考虑到线程栈的问题,多进程在并发上有优点。

4、多进程和多线程在建立和销毁上的效率比较

预先建立进程或线程能够节省进程或线程的建立、销毁时间,在实际的应用中不少程序使用了这样的策略,好比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):

  1. #include <stdio.h>
  2. #include <signal.h>
  3. #include <stdio.h>
  4. #include <unistd.h>
  5. #include <sys/stat.h>
  6. #include <fcntl.h>
  7. #include <sys/types.h>
  8. #include <sys/wait.h>

  9. int count;//子进程建立成功数量 
  10. int fcount;//子进程建立失败数量 
  11. int scount;//子进程回收数量 

  12. /*信号处理函数–子进程关闭收集*/
  13. void sig_chld(int signo)
  14. {
  15.     pid_t chldpid;//子进程id
  16.     int stat;//子进程的终止状态
  17.     
  18.     //子进程回收,避免出现僵尸进程
  19.     while((chldpid=wait(&stat)>0))
  20.     {
  21.         scount++;
  22.     }
  23. }

  24. int main()
  25. {
  26.     //注册子进程回收信号处理函数
  27.     signal(SIGCHLD,sig_chld);
  28.     
  29.     int i;
  30.     for(i=0;i<100000;i++)//fork()10万个子进程
  31.     {
  32.         pid_t pid=fork();
  33.         if(pid==-1)//子进程建立失败
  34.         {
  35.             fcount++;
  36.         }
  37.         else if(pid>0)//子进程建立成功
  38.         {
  39.             count++;
  40.         }
  41.         else if(pid==0)//子进程执行过程
  42.         {
  43.             exit(0);
  44.         }
  45.     }
  46.     
  47.     printf("count:%d fount:%d scount:%d\n",count,fcount,scount);
  48. }

建立10万个线程(pthreadcreat.c):

  1. #include <stdio.h>
  2. #include <pthread.h>

  3. int count=0;//成功建立线程数量

  4. void thread(void)
  5. {
  6.     //啥也不作
  7. }

  8. int main(void)
  9. {
  10.     pthread_t id;//线程id
  11.     int i,ret;
  12.     
  13.     for(i=0;i<100000;i++)//建立10万个线程
  14.     {
  15.         ret=pthread_create(&id,NULL,(void *)thread,NULL);
  16.         if(ret!=0)
  17.         {
  18.             printf("Create pthread error!\n");
  19.             return(1);
  20.         }
  21.         count++;
  22.         pthread_join(id,NULL);
  23.     }
  24.     
  25.     printf("count:%d\n",count);
  26. }

建立10万个线程的Java程序:

  1. public class ThreadTest
  2.     {
  3.         public static void main(String[] ags) throws InterruptedException
  4.         {
  5.             System.out.println("开始运行");
  6.             long start = System.currentTimeMillis();
  7.             for(int i = 0; i < 100000; i++) //建立10万个线程
  8.             {
  9.                 Thread athread = new Thread(); //建立线程对象
  10.                 athread.start(); //启动线程
  11.                 athread.join(); //等待该线程中止
  12.             }
  13.            
  14.             System.out.println("用时:" + (System.currentTimeMillis() – start) + " 毫秒");
  15.         }
  16.     }

单核(双核机器禁掉一核),建立销毁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迷们最愿意看到的结论了。

5、双核系统重复周丽论文实验步骤

双核,进程/线程数: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)


【实验结论】

双核处理器在完成任务量较少时,没有系统其余瓶颈因素影响时基本上是单核的两倍,在任务量较多时,受系统其余瓶颈因素的影响,速度明显趋近于单核的速度。