多进程与多线程差别

Posted llguanli

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了多进程与多线程差别相关的知识,希望对你有一定的参考价值。



在Unix上编程採用多线程还是多进程的争执由来已久,这样的争执最常见到在C/S通讯中服务端并发技术 的选型上,比方WEBserver技术中。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。我的环境是我的工作本本:单cpu单核celeron(R) M 1.5 GZ,1.5G内存,ubuntu10.04 desktop,内核2.6.32。

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

  1. #include <stdlib.h>
  2. #include <stdio.h>
  3. #include <signal.h>
  4.  
  5. #define P_NUMBER 255    /* 并发进程数量 */
  6. #define COUNT 100       /* 每进程打印字符串次数 */
  7. #define TEST_LOGFILE "logFile.log"
  8. FILE *logFile = NULL;
  9.  
  10. char *s = "hello linux\0";
  11.  
  12. int main()
  13. {
  14.     int i = 0,j = 0;
  15.     logFile = fopen(TEST_LOGFILE, "a+"); /* 打开日志文件 */
  16.     for(i = 0; i < P_NUMBER; i++)
  17.     {
  18.         if(fork() == 0) /* 创建子进程,if(fork() == 0){}这段代码是子进程执行区间 */
  19.         {
  20.             for(j = 0;j < COUNT; j++)
  21.             {
  22.                 printf("[%d]%s\n", j, s); /* 向控制台输出 */
  23.                 fprintf(logFile,"[%d]%s\n", j, s); /* 向日志文件输出 */
  24.             }
  25.             exit(0); /* 子进程结束 */
  26.         }
  27.     }
  28.  
  29.     for(i = 0; i < P_NUMBER; i++) /* 回收子进程 */
  30.     {
  31.         wait(0);
  32.     }
  33.  
  34.     printf("OK\n");
  35.     return 0;
  36. }

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

  1. #include <pthread.h>
  2. #include <unistd.h>
  3. #include <stdlib.h>
  4. #include <stdio.h>
  5.  
  6. #define P_NUMBER 255    /* 并发线程数量 */
  7. #define COUNT 100       /* 每线程打印字符串次数 */
  8. #define Test_Log "logFIle.log"
  9. FILE *logFile = NULL;
  10.  
  11. char *s = "hello linux\0";
  12.  
  13. print_hello_linux() /* 线程运行的函数 */
  14. {
  15.     int i = 0;
  16.     for(i = 0; i < COUNT; i++)
  17.     {
  18.         printf("[%d]%s\n", i, s); /* 向控制台输出 */
  19.         fprintf(logFile, "[%d]%s\n", i, s); /* 向日志文件输出 */
  20.     }
  21.     pthread_exit(0); /* 线程结束 */
  22. }
  23.  
  24. int main()
  25. {
  26.     int i = 0;
  27.     pthread_t pid[P_NUMBER]; /* 线程数组 */
  28.     logFile = fopen(Test_Log, "a+"); /* 打开日志文件 */
  29.  
  30.     for(i = 0; i < P_NUMBER; i++)
  31.         pthread_create(&pid[i], NULL, (void *)print_hello_linux, NULL); /* 创建线程 */
  32.  
  33.     for(i = 0; i < P_NUMBER; i++)
  34.         pthread_join(pid[i],NULL); /* 回收线程 */
  35.  
  36.     printf("OK\n");
  37.     return 0;
  38. }

两段程序做的事情是一样的,都是创建“若干”个进程/线程,每一个创建出的进程/线程打印“若干”条“hello linux”字符串到控制台和日志文件。两个“若干”由两个宏 P_NUMBER和COUNT分别定义,程序编译指令例如以下:

[email protected]:~/tmp1$ gcc -o fork fork.c
[email protected]:~/tmp1$ gcc -lpthread -o thread thread.c

实验通过time指令运行两个程序。抄录time输出的挂钟时间(real时间):

time ./fork
time ./thread

每批次的实验通过修改宏 P_NUMBER和COUNT来调整进程/线程数量和打印次数,每批次測试五轮,得到的结果例如以下:

一、反复周丽论文实验步骤

  第1次 第2次 第3次 第4次 第5次 平均
多进程 0m1.277s 0m1.175s 0m1.227s 0m1.245s 0m1.228s 0m1.230s
多线程 0m1.150s 0m1.192s 0m1.095s 0m1.128s 0m1.177s 0m1.148s

进程线程数:255 / 打印次数:100

  第1次 第2次 第3次 第4次 第5次 平均
多进程 0m6.341s 0m6.121s 0m5.966s 0m6.005s 0m6.143s 0m6.115s
多线程 0m6.082s 0m6.144s 0m6.026s 0m5.979s 0m6.012s 0m6.048s

进程线程数:255 / 打印次数:500

  第1次 第2次 第3次 第4次 第5次 平均
多进程 0m12.155s 0m12.057s 0m12.433s 0m12.327s 0m11.986s 0m12.184s
多线程 0m12.241s 0m11.956s 0m11.829s 0m12.103s 0m11.928s 0m12.011s

进程线程数:255 / 打印次数:1000

  第1次 第2次 第3次 第4次 第5次 平均
多进程 1m2.182s 1m2.635s 1m2.683s 1m2.751s 1m2.694s 1m2.589s
多线程 1m2.622s 1m2.384s 1m2.442s 1m2.458s 1m3.263s 1m2.614s

进程线程数:255 / 打印次数:5000

本轮实验是为了和周丽论文作对照,因此将进程/线程数量限制在255个,论文也是測试了255个进程/线程分别进行10 次,50 次,100 次,200 次……900 次打印的用时,论文得出的结果是:任务量较大时,多进程比多线程效率高;而完毕的任务量较小时,多线程比多进程要快,反复打印 600 次时,多进程与多线程所耗费的时间同样。

尽管我的实验直到5000打印次数时,多进程才開始率先,但考虑到使用的是NPTL线程库的缘故,从而能够证实了论文的观点。从我的实验数据看,多线程和多进程两组数据很接近。考虑到数据的提取具有瞬间性。因此能够觉得他们的速度是同样的。

当前的网络环境中。我们更看中高并发、高负荷下的性能,纵观前面的实验步骤。最长的实验周期只是1分钟多一点,因此以下的实验将向两个方向延伸。第一,添加并发数量,第二。添加每进程/线程的工作强度。

二、添加并发数量的实验

以下的实验打印次数不变。而进程/线程数量逐渐添加。

在实验过程中多线程程序在后三组(线程数500,800。1000)的測试中都出现了“段错误”。出现错误的原因和线程栈的大小有关。

实验中的计算机CPU是32位的赛扬,寻址最大范围是4GB(2的32次方),Linux是依照3GB/1GB的方式来分配内存。当中1GB属于所 有进程共享的内核空间,3GB属于用户空间(进程虚拟内存空间)。对于进程而言仅仅有一个栈,这个栈能够用尽这3GB空间(计算时须要排除程序文本、数据、 共享库等占用的空间)。所以它的大小通常不是问题。但对线程而言每一个线程有一个线程栈。这3GB空间会被全部线程栈摊分,线程数量太多时,线程栈累计的大 小将超过进程虚拟内存空间大小,这就是实验中出现的“段错误”的原因。

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

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

[email protected]:~/tmp1$ ulimit -s 1024
[email protected]:~/tmp1$ time ./thread

  第1次 第2次 第3次 第4次 第5次 平均
多进程 0m4.958s 0m5.032s 0m5.181s 0m4.951s 0m5.032s 0m5.031s
多线程 0m4.994s 0m5.040s 0m5.071s 0m5.113s 0m5.079s 0m5.059s

进程线程数:100 / 打印次数:1000

  第1次 第2次 第3次 第4次 第5次 平均
多进程 0m12.155s 0m12.057s 0m12.433s 0m12.327s 0m11.986s 0m12.184s
多线程 0m12.241s 0m11.956s 0m11.829s 0m12.103s 0m11.928s 0m12.011s

进程线程数:255 / 打印次数:1000 (这里使用了第一次的实验数据)

  第1次 第2次 第3次 第4次 第5次 平均
多进程 0m17.686s 0m17.569s 0m17.609s 0m17.663s 0m17.784s 0m17.662s
多线程 0m17.694s 0m17.976s 0m17.884s 0m17.785s 0m18.261s 0m17.920s

进程线程数:350 / 打印次数:1000

  第1次 第2次 第3次 第4次 第5次 平均
多进程 0m23.638s 0m23.543s 0m24.135s 0m23.981s 0m23.507s 0m23.761s
多线程 0m23.634s 0m23.326s 0m23.408s 0m23.294s 0m23.980s 0m23.528s

进程线程数:500 / 打印次数:1000 (线程栈大小更改为1M)

  第1次 第2次 第3次 第4次 第5次 平均
多进程 0m38.517s 0m38.133s 0m38.872s 0m37.971s 0m38.005s 0m38.230s
多线程 0m38.011s 0m38.049s 0m37.635s 0m38.219s 0m37.861s 0m37.995s

进程线程数:800 / 打印次数:1000 (线程栈大小更改为1M)

  第1次 第2次 第3次 第4次 第5次 平均
多进程 0m48.157s 0m47.921s 0m48.124s 0m48.081s 0m48.126s 0m48.082s
多线程 0m47.513s 0m47.914s 0m48.073s 0m47.920s 0m48.652s 0m48.014s

进程线程数:1000 / 打印次数:1000 (线程栈大小更改为1M)

出现了线程栈的问题,让我特别关心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. }
  第1次 第2次 第3次 第4次 第5次 平均
Java 65664 毫秒 66269 毫秒 65546 毫秒 65931 毫秒 66540 毫秒 65990 毫秒

线程数:1000 / 打印次数:1000

Java程序比C程序慢一些在情理之中,但Java程序并没有出现线程栈问题,5次測试都平稳完毕,能够用以下的ps指令获得java进程中线程的数量:

[email protected]:~$ 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的默认大小一致:

[email protected]:~/tmp1$ java -Xss8192k MyThread

出乎意料的是并没有出现想象中的异常,但用ps侦測线程数最高到达337。我推断程序在创建线程时在栈到达可用内存的上线时就停止继续创建了,程序 执行的时间远小于预计值也证明了这个推断。程序尽管没有抛出异常,但执行的并不正常。还有一个问题是最后并没有打印出“用时 xxx毫秒”信息。

这次測试更加深了我的一个长期的推測:Java的Web容器不稳定。

由于我是多年编写B/S的Java程序猿。WEB服务不稳定经常挂掉也是司空见 惯的,除了自己或项目组成员水平不高,代码编写太烂的原因之外,我一直推測还有更深层的原因。假设就是线程原因的话,这颠覆性可比本篇文章的多进程性能颠 覆性要大得多,想想世界上有多少Tomcat、Jboss、Websphere、weblogic在跑着,嘿嘿。

这次測试还打破了曾经的一个说法:单CPU上并发超过6、7百。线程或进程间的切换就会占用大量CPU时间,造成server效率会急剧下降。但从上面的实验来看。进程/线程数到1000时(这几乎相同是非常繁忙的WEBserver了),仍具有非常好的线性。

三、添加每进程/线程的工作强度的实验

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

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

如今改动为每次打印256个字节数据:

  1. char *s = "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";
  第1次 第2次 第3次 第4次 第5次 平均
多进程 0m28.149s 0m27.993s 0m28.094s 0m27.657s 0m28.016s 0m27.982s
多线程 0m28.171s 0m27.764s 0m27.865s 0m28.041s 0m27.780s 0m27.924s

进程线程数:255 / 打印次数:100

  第1次 第2次 第3次 第4次 第5次 平均
多进程 2m20.357s 2m19.740s 2m19.965s 2m19.788s 2m19.796s 2m19.929s
多线程 2m20.061s 2m20.462s 2m19.789s 2m19.514s 2m19.479s 2m19.861s

进程线程数:255 / 打印次数:500

  第1次 第2次
多进程 9m39s 9m17s
多线程 9m31s 9m22s

进程线程数:255 / 打印次数:2000 (实验太耗时,因此仅仅进行了2轮比对)

【实验结论】

从上面的实验比对结果看,即使Linux2.6使用了新的NPTL线程库(据说比原线程库性能提高了非常多,唉,又是据说!

),多线程比較多进程在效率上没有不论什么的优势,在线程数增大时多线程程序还出现了执行错误,实验能够得出以下的结论:

在Linux2.6上。多线程并不比多进程速度快,考虑到线程栈的问题。多进程在并发上有优势。

四、多进程和多线程在创建和销毁上的效率比較

预先创建进程或线程能够节省进程或线程的创建、销毁时间,在实际的应用中非常多程序使用了这种策略,比方Apapche预先创建进程、Tomcat 预先创建线程,通常叫做进程池或线程池。在大部分人的概念中。进程或线程的创建、销毁是比較耗时的,在stevesn的著作《Unix网络编程》中有这样 的对照图(第一卷 第三版 30章 客户/server程序设计范式):

行号 server描写叙述 进程控制CPU时间(秒,与基准之差)
Solaris2.5.1 Digital Unix4.0b BSD/OS3.0
0 迭代server(基准測试,无进程控制) 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比較了三种server上多进程和多线程的运行效 率。由于三种server所用计算机不同,表中数据仅仅能纵向比較。而横向无可比性,stevens在书中提供了这些測试程序的源代码(也能够在网上下载)。书中介 绍了測试环境,两台与server处于同一子网的客户机。每一个客户并发5个进程(server同一时间最多10个连接)。每一个客户请求从server获取4000字节数据。 预先派生子进程或线程的数量是15个。

第0行是迭代模式的基准測试程序,server程序仅仅有一个进程在执行(同一时间仅仅能处理一个客户请求),由于没有进程或线程的调度切换。因此它的速度是 最快的,表中其它服务模式的执行数值是比迭代模式多出的差值。

迭代模式非常少用到。在现有的互联网服务中。DNS、NTP服务有它的影子。第1~5行是多进 程服务模式。期中第1行使用现场fork子进程。2~5行都是预先创建15个子进程模式,在多进程程序中套接字传递不太easy(相对于多线程)。 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没有提供server程序总体的执行计时。我们无法理解504.2 :21.5的实际执行效率,有可能是1504.2 : 1021.5。也可能是100503.2 : 100021.5,20倍的差异可能非常大。也可能能够忽略。

因此我写了以下的实验程序,来计算在Linux2.6上创建、销毁10万个进程/线程的绝对用时。

创建10万个进程(forkcreat.c):

  1. #include <stdlib.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.  
  10. int count;  /* 子进程创建成功数量 */
  11. int fcount; /* 子进程创建失败数量 */
  12. int scount; /* 子进程回收数量 */
  13.  
  14. /* 信号处理函数–子进程关闭收集 */
  15. void sig_chld(int signo)
  16. {
  17.    
  18.     pid_t chldpid; /* 子进程id */
  19.     int stat; /* 子进程的终止状态 */
  20.  
  21.     /* 子进程回收。避免出现僵尸进程 */
  22.     while ((chldpid = wait(&stat)) > 0)
  23.     {
  24.         scount++;
  25.     }
  26. }
  27.  
  28. int main()
  29. {
  30.     /* 注冊子进程回收信号处理函数 */
  31.     signal(SIGCHLD, sig_chld);
  32.  
  33.     int i;
  34.     for (i = 0; i < 100000; i++) //fork()10万个子进程
  35.     {
  36.         pid_t pid = fork();
  37.         if (pid == -1) //子进程创建失败
  38.         {
  39.             fcount++;
  40.         }
  41.         else if (pid > 0) //子进程创建成功
  42.         {
  43.             count++;
  44.         }
  45.         else if (pid == 0) //子进程运行过程
  46.         {
  47.             exit(0);
  48.         }
  49.     }
  50.  
  51.     printf("count: %d fcount: %d scount: %d\n", count, fcount, scount);
  52. }

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

  1. #include <stdio.h>
  2. #include <pthread.h>
  3.  
  4. int count = 0; /* 成功创建线程数量 */
  5.  
  6. void thread(void)
  7. {
  8.     /* 线程啥也不做 */
  9. }
  10.  
  11. int main(void)
  12. {
  13.     pthread_t id; /* 线程id */
  14.     int i,ret;
  15.  
  16.     for (i = 0; i < 100000; i++) /* 创建10万个线程 */
  17.     {
  18.         ret = pthread_create(&id, NULL, (void *)thread, NULL);
  19.         if(ret != 0)
  20.         {
  21.             printf ("Create pthread error!\n");
  22.             return (1);
  23.         }
  24.  
  25.         count++;
  26.  
  27.         pthread_join(id, NULL);
  28.     }
  29.    
  30.     printf("count: %d\n", count);
  31.  
  32. }

创建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. }

在我的赛扬1.5G的CPU上測试结果例如以下(仍採用測试5次后计算平均值):

创建销毁10万个进程 创建销毁10万个线程 创建销毁10万个线程(Java)
0m18.201s 0m3.159s 12286毫秒

从数据能够看出,多线程比多进程在效率上有5~6倍的优势,但不能让我们在使用那种并发模式上定性,这让我想起多年前政治课上的一个场景:在讲到优 越性时,面对着几个对此发表质疑评论的调皮男生,我们的政治老师发表了高见,“不能仅仅横向地和当今的发达国家比,你应该纵向地和过去中国几十年的发展历史 比”。政治老师的话套用在当前简直就是真理。我们看看,即使是在赛扬CPU上,创建、销毁进程/线程的速度都是空前的,能够说是有质的飞跃的,平均创建销 毁一个进程的速度是0.18毫秒,对于当前server几百、几千的并发量,还有预先派生子进程/线程的必要吗?

预先派生子进程/线程比现场创建子进程/线程要复杂非常多。不仅要对池中进程/线程数量进行动态管理,还要解决多进程/多线程对accept的“抢” 问题,在stevens的測试程序中,使用了“惊群”和“锁”技术。

即使stevens的数据表格中。预先派生线程也不见得比现场创建线程快。在 《Unix网络编程》第三版中。新作者參照stevens的測试也提供了一组数据。在这组数据中,现场创建线程模式比预先派生线程模式已有了效率上的优 势。因此我对这一节实验下的结论是:

预先派生进程/线程的模式(进程池、线程池)技术。不仅复杂,在效率上也无优势。在新的应用中能够放心大胆地为客户连接请求去现场创建进程和线程。

我想,这是fork迷们最愿意看到的结论了。

五、并发服务的不可測性

看到这里。你会感觉到我有挺进程、贬线程的论调。实际上对于现实中的并发服务具有不可測性,前面的实验和结论仅仅可做參考,而不可定性。对于不可測性。我举个生活中的样例。

这几年在大都市生活的朋友都感觉城市交通状况越来越差。到处堵车。从好的方面想这不正反应了我国GDP的快速发展。假设你7、8年前来到西安市,穿 过南二环上的一些十字路口时,会发现一个奇怪的U型弯的交通管制,为了更好的说明。我画了两张图来说明,第一张图是採用U型弯之前的。第二张是採用U型弯 之后的。

 

南二环交通图一

 

南二环交通图二

为了讲述的方便。我们不考虑十字路口左拐的情况。在图一中东西向和南北向的车辆交汇在十字路口,用红绿灯控制同一时间仅仅能东西向或南北向通行,一般 的十字路口都是这样管控的。随着车辆的增多。十字路口的阻塞越来越严重。尤其是上下班时间常常出现堵死现象。于是交通部门在不动用过多经费的情况下而採用 了图二的交通管制。东西向车辆行进方式不变。而南北向车辆不能直行,须要右拐到下一个路口拐一个超大的U型弯,这种措施避免了因车辆交错而引发堵死的次 数,从而提高了车辆的通过效率。

我以前问一个每天上下班乘公交经过此路口的同事,他说这种修改不一定每次上下班时间都能缩短,但上班时间有保障了,从而 迟到次数降低了。假设今天你去西安市的南二环已经见不到U型弯了。东西向建设了高架桥。车辆分流后下层的十字路口已恢复为图一方式。

从效率的角度分析。在图一中等一个红灯45秒,远远小于图二拐那个U型弯用去的时间。但实际情况正好相反。我们能够设想一下。假设路上的全部执行车 辆都是同一型号(比方说全是QQ3微型车)。全部的司机都遵守交规。具有相同的心情和性格,那么图一的通行效率肯定比图二高。

现实中就不一样了,首先车辆 不统一,有大车、小车、快车、慢车,其次司机的品格不一,有特别遵守交规的。有想耍点小聪明的。有性子慢的,也有的性子急,时不时还有三轮摩托逆行一下, 十字路口的“死锁”也就难免了。

那么在什么情况下图二优于图一,能否拿出一个科学分析数据来呢?以如今的科学技术水平是拿不出来的,就像长期的天气预报不可预測一样,西安市的交管部门肯定不是分析各种车辆的执行规律、速度,再进行复杂的社会学、心理学分析做出U型弯的决定的,这就是要说的不可測性。

现实中的程序亦然如此。比方WEBserver,有的客户在快车道(宽带),有的在慢车道(窄带)。有的性子慢(等待半分钟也无所谓),有的性子急(拼命 的进行浏览器刷新)。时不时另一两个黑客混入当中,这样的情况每一个server都不一样,既是是同一server每时每刻的变化也不一样,因此说不具有可測性。

开发人员 和维护者能做的,不论是前面的这样的实验測试,还是对详细站点进行的压力測试,最多也就能模拟相当于QQ3通过十字路口的场景。

结束语

本篇文章比較了Linux系统上多线程和多进程的执行效率。在实际应用时还有其它因素的影响。比方网络通讯时採用长连接还是短连接,是否採用 select、poll,java中称为nio的机制。还有使用的编程语言,比如Java不能使用多进程。php不能使用多线程,这些都可能影响到并发模 式的选型。

以上是关于多进程与多线程差别的主要内容,如果未能解决你的问题,请参考以下文章

多线程与多进程介绍

Python多线程与多进程

python多进程与多线程使用场景

python之多线程与多进程

多进程(mutiprocessing)与多线程(Threading)之多线程

多进程与多线程