传统多任务操作系统中一个可以独立调度的任务(或称之为顺序执行流)是一个进程。每个程序加载到内存后只可以唯一地对应创建一个顺序执行流,即传统意义的进程。每个进程的全部系统资源是私有的,如虚拟地址空间,文件描述符和信号处理等等。使用多进程实现多任务应用时存在如下问题:
1)任务切换,即进程间上下文切换,系统开销比较大。(虚拟地址空间以及task_struct 都需要切换)
2)多任务之间的协作比较麻烦,涉及进程间通讯。(因为不同的进程工作在不同的地址空间)
所以,为了提高系统的性能,许多操作系统规范里引入了轻量级进程的概念,也被称为线程。
通常线程指的是共享相同地址空间的多个任务。线程最大的特点就是在同一个进程中创建的线程共享该进程的地址空间;但一个线程仍用task_struct 来描述,线程和进程都参与统一的调度。所以,多线程的好处便体现出来:
1)大大提高了任务切换的效率;因为各线程共享进程的地址空间,任务切换时只要切换task_struct 即可;
2)线程间通信比较方便;因为在同一块地址空间,数据共享;
当然,共享地址空间也会成为线程的缺点,因为共享地址空间,如果其中一个线程出现错误(比如段错误),整个线程组都会崩掉!
linux之所以称呼其线程为LWP( Light Weight Process ),因为从内核实现的角度来说,它并没有为线程单独创建一个结构,而是继承了很多进程的设计:
1)继承了进程的结构体定义task_struct ;
2)没有专门定义线程ID,复用了PID;
3)更没有为线程定义特别的调度算法,而是沿用了原来对task_struct 的调度算法。
在最新的Linux内核里线程已经替代原来的进程称为调度的实际最小单位。
原来的进程概念可以看成是多个线程的容器,称之为线程组;即一个进程就是所有相关的线程构成的一个线程组。传统的进程等价于单线程进程。
每个线程组都有自己的标识符 tgid (数据类型为 pid_t ),其值等于该进程(线程组)中的第一个线程(group_leader)的PID。
pthread_create()函数描述如下:
1)这里routine 是回调函数(callback),其函数类型由内核来决定,这里我们将其地址传给内核;这个函数并不是线程创建了就会执行,而是只有当其被调度到cpu上时才会被执行;具体回调函数的讲解,移步Linux C 函数指针应用—回调函数 .;
2)arg 是线程执行函数的参数,这里我们将其地址穿进去,使用时需要先进行类型转换,才能使用;如果参数不止一个,我们可以将其放入到结构体中。
其函数描述如下:
这里,我们可以看到 value_ptr 是个二级指针,其是出参,存放的是线程返回参数的地址;
其函数描述如下:
和进程中的exit() 、wait()一样,这里pthread_join 与 pthread_exit 是工作在两个线程之中;
下面看一个实例:
【参见附件/thread.c】
#include
#include
#include
#include
char message[32] = "Hello World!";
void *thread_function(void *arg);
int main()
{
pthread_t a_thread;
void *thread_result;
if(pthread_create(&a_thread,NULL,thread_function,(void *)message) < 0)
{
perror("fail to pthread_create");
exit(-1);
}
printf("waiting for thread to finish\n");
if(pthread_join(a_thread,&thread_result) < 0)
{
perror("fail to pthread_join");
exit(-1);
}
printf("Message is now %s\n",message);
printf("thread_result is %s\n",(char *)thread_result);
return 0;
}
void *thread_function(void *arg)
{
printf("thread_function is running,argument is %s\n",(char *)arg);
strcpy(message,"marked by thread");
pthread_exit("Thank you for the cpu time");
}
编译:
线程通过第三方的线程库来实现,所以这里要 -lpthread ,-l 是链接一个库,这个库是pthread;
执行结果如下:
从这个程序,我们可以看到线程之间是如何通信的,线程之间通过二级指针来传送参数的地址(这是进程所不具备的,因为他们的地址空间独立),但两个线程之间的通信,传递的数据的生命周期必须是静态的。可以使全局变量、static修饰的数据、堆里面的数据;这个程序中的message就是一个全局变量。其中一个线程可以修改它,另一个线程得到它修改过后的message。
【小贴士】
获取线程ID的函数
先来了解同步和互斥的基本概念:
临界资源:某些资源来说,其在同一时间只能被一段机器指令序列所占用。这些一次只能被一段指令序列所占用的资源就是所谓的临界资源。
临界区:对于临界资源的访问,必须是互斥进行。也就是当临界资源被一个指令序列占用时,另一个需要访问相同临界资源的指令序列就不能被执行。指令序列不能执行的实际意思就是其所在的进程/线程会被阻塞。所以我们定义程序内访问临界资源的代码序列被称为临界区。
互斥:是指同事只允许一个访问者对临界资源进行访问,具有唯一性和排它性。但互斥无法限制访问这个对资源的访问顺序,即访问时无序的。
同步:是指在互斥的基础上,通过其他机制实现访问者对资源的有序访问。
引入互斥(mutual exlusion)锁的目的是用来保证共享数据的完整性。
互斥锁主要用来保护临界资源。每个临界资源都有一个互斥锁来保护,任何时刻最多只能有一个线程能访问该资源;线程必须先获得互斥锁才能访问临界资源,访问完资源后释放该锁。如果无法获得锁,线程会阻塞直到获得锁为止;
通常,我们在临界区前上锁,临界区后解锁;
1) 初始化互斥锁函数
2)申请互斥锁函数
3)释放互斥锁函数
下面是一个实例:
【参见附件/mutex.c】
#include
#include
#include
#include
#include
//#define _LOCK_
unsigned int value1,value2,count;
pthread_mutex_t mutex;
void *function(void *arg);
int main()
{
pthread_t a_thread;
if(pthread_mutex_init(&mutex,NULL) < 0)
{
perror("fail to mutex_init");
exit(-1);
}
if(pthread_create(&a_thread,NULL,function,NULL) != 0)
{
perror("fail to pthread_create");
exit(-1);
}
while(1)
{
count++;
#ifdef _LOCK_
pthread_mutex_lock(&mutex);
#endif
value1 = count;
value2 = count;
#ifdef _LOCK_
pthread_mutex_unlock(&mutex);
#endif
}
return 0;
}
void *function(void *arg)
{
while(1)
{
#ifdef _LOCK_
pthread_mutex_lock(&mutex);
#endif
if(value1 != value2)
{
printf("count = %d,value1 = %d,value2 = %d\n",count,value1,value2);
usleep(100000);
}
#ifdef _LOCK_
pthread_mutex_unlock(&mutex);
#endif
}
return NULL;
}
执行结果如下:
我们可以看到,数据是不断被打印的,说明 a 线程是可以访问临界资源的。
我们把#define _LOCK_前面的注释去掉,这时就加上了互斥锁,执行程序,此时,并没有数据被打印,说明此时a线程中 value1 与 value 2 一直是相等的,说明主线程执行是,a线程并无法访问临界资源的。
同步(synchronization) 指的是多个任务(线程)按照约定的顺序相互配合完成一件事情;
线程间同步——P / V 操作
信号量代表某一类资源,其值表示系统中该资源当前可用的数量。
信号量是一个受保护的变量,只能通过三种操作来访问:
1)初始化
2)P操作(申请资源)
3)V操作(释放资源)
P(S)含义如下:
if (信号量的值大于0)
{
请资源的任务继续运行;
信号量的值 减一;
}
else
{
请资源的任务阻塞;
}
V(S)含义如下:
if (没有任务在等待该资源)
{
信号量的值 加一;
}
else
{
唤醒第一个等待的任务,让其继续运行;
}
1)、信号量初始化函数:
2) P操作
3)V操作
下面是个实例:
【参见附件/sem.c】
#include
#include
#include
#include
#include
char buf[60];
sem_t sem;
void *function(void *arg);
int main(int argc, char *argv[])
{
pthread_t a_thread;
void *thread_result;
if(sem_init(&sem,0,0) != 0)
{
perror("fail to sem_init");
exit(-1);
}
if(pthread_create(&a_thread,NULL,function,NULL) != 0)
{
perror("fail to pthread_create");
exit(-1);
}
printf("input 'quit' to exit\n");
do
{
fgets(buf,60,stdin);
sem_post(&sem);
}
while(strncmp(buf,"quit",4) != 0);
return 0;
}
void *function(void *arg)
{
while(1)
{
sem_wait(&sem);
printf("you enter %d characters\n",strlen(buf) - 1);
}
}
1)计算机的核心是CPU,它承担了所有的计算任务。它就像一座工厂,时刻在运行。
2)假定工厂的电力有限,一次只能供给一个车间使用。也就是说,一个车间开工的时候,其他车间都必须停工。背后的含义就是,单个CPU一次只能运行一个任务。
3)进程就好比工厂的车间,它代表CPU所能处理的单个任务。任一时刻,CPU总是运行一个进程,其他进程处于非运行状态。
4)一个车间里,可以有很多工人。他们协同完成一个任务。
5)线程就好比车间里的工人。一个进程可以包括多个线程。
6)车间的空间是工人们共享的,比如许多房间是每个工人都可以进出的。这象征一个进程的内存空间是共享的,每个线程都可以使用这些共享内存。
7)可是,每间房间的大小不同,有些房间最多只能容纳一个人,比如厕所。里面有人的时候,其他人就不能进去了。这代表一个线程使用某些共享内存时,其他线程必须等它结束,才能使用这一块内存。
8)一个防止他人进入的简单方法,就是门口加一把锁。先到的人锁上门,后到的人看到上锁,就在门口排队,等锁打开再进去。这就叫"互斥锁"(Mutual exclusion,缩写 Mutex),防止多个线程同时读写某一块内存区域。
9)还有些房间,可以同时容纳n个人,比如厨房。也就是说,如果人数大于n,多出来的人只能在外面等着。这好比某些内存区域,只能供给固定数目的线程使用。
10)这时的解决方法,就是在门口挂n把钥匙。进去的人就取一把钥匙,出来时再把钥匙挂回原处。后到的人发现钥匙架空了,就知道必须在门口排队等着了。这种做法叫做"信号量"(Semaphore),用来保证多个线程不会互相冲突。
不难看出,mutex是semaphore的一种特殊情况(n=1时)。也就是说,完全可以用后者替代前者。但是,因为mutex较为简单,且效率高,所以在必须保证资源独占的情况下,还是采用这种设计。
11)操作系统的设计,因此可以归结为三点:
(1)以多进程形式,允许多个任务同时运行;
(2)以多线程形式,允许单个任务分成不同的部分运行;
(3)提供协调机制,一方面防止进程之间和线程之间产生冲突,另一方面允许进程之间和线程之间共享资源。
在Linux下编程多用多进程编程少用多线程编程。
IBM有个家伙做了个测试,发现切换线程context的时候,windows比linux快一倍多。进出最快的锁(windows的 critical section和linux的pthread_mutex),windows比linux的要快五倍左右。当然这并不是说linux不好,而且在经过实际编程之后,综合来看我觉得linux更适合做high performance server,不过在多线程这个具体的领域内,linux还是稍逊windows一点。这应该是情有可原的,毕竟unix家族都是从多进程过来的,而 windows从头就是多线程的。
如果是UNIX/linux环境,采用多线程没必要。
多线程比多进程性能高?误导!应该说,多线程比多进程成本低,但性能更低。
在UNIX环境,多进程调度开销比多线程调度开销,没有显著区别,就是说,UNIX进程调度效率是很高的。内存消耗方面,二者只差全局数据区,现在内存都很便宜,服务器内存动辄若干G,根本不是问题。
多进程是立体交通系统,虽然造价高,上坡下坡多耗点油,但是不堵车。多线程是平面交通系统,造价低,但红绿灯太多,老堵车。
我们现在都开跑车,油(主频)有的是,不怕上坡下坡,就怕堵车。高性能交易服务器中间件,如TUXEDO,都是主张多进程的。实际测试表明,TUXEDO性能和并发效率是非常高的。TUXEDO是贝尔实验室的,与UNIX同宗,应该是对UNIX理解最为深刻的,他们的意见应该具有很大的参考意义。
多线程的优点:
【1】无需跨进程边界;
【2】程序逻辑和控制方式简单;
【3】所有线程可以直接共享内存和变量等;
【4】线程方式消耗的总资源比进程方式好;
多线程缺点:
【1】每个线程与主程序共用地址空间,受限于2GB地址空间;
【2】线程之间的同步和加锁控制比较麻烦;
【3】一个线程的崩溃可能影响到整个程序的稳定性;
【4】到达一定的线程数程度后,即使再增加CPU也无法提高性能,例如Windows Server 2003,大约是1500个左右的线程数就快到极限了(线程堆栈设定为1M),如果设定线程堆栈为2M,还达不到1500个线程总数;
【5】线程能够提高的总性能有限,而且线程多了之后,线程本身的调度也是一个麻烦事儿,需要消耗较多的CPU 。
多进程优点:
【1】每个进程互相独立,不影响主程序的稳定性,子进程崩溃没关系;
【2】通过增加CPU,就可以容易扩充性能;
【3】可以尽量减少线程加锁/解锁的影响,极大提高性能,就算是线程运行的模块算法效率低也没关系;
【4】每个子进程都有2GB地址空间和相关资源,总体能够达到的性能上限非常大
多线程缺点:
【1】逻辑控制复杂,需要和主程序交互;
【2】需要跨进程边界,如果有大数据量传送,就不太好,适合小数据量传送、密集运算 ;
【3】多进程调度开销比较大;
最好是多进程和多线程结合,即根据实际的需要,每个CPU开启一个子进程,这个子进程开启多线程可以为若干同类型的数据进行处理。当然你也可以利用多线程+多CPU+轮询方式来解决问题……
方法和手段是多样的,关键是自己看起来实现方便有能够满足要求,代价也合适。
进程的优点:
【1】顺序程序的特点:具有封闭性和可再现性;
【2】程序的并发执行和资源共享。多道程序设计出现后,实现了程序的并发执行和资源共享,提高了系统的效率和系统的资源利用率。
进程的缺点:
【1】操作系统调度切换多个线程要比切换调度进程在速度上快的多。而且进程间内存无法共享,通讯也比较麻烦。
【2】线程之间由于共享进程内存空间,所以交换数据非常方便;在创建或撤消进程时,由于系统都要为之分配和回收资源,导致系统的开销明显大于创建或撤消线程时的开销。
线程的优点:
【1】它是一种非常"节俭"的多任务操作方式。在Linux系统下,启动一个新的进程必须分配给它独立的地址空间,建立众多的数据表来维护它的代码段、堆栈段和数据段,这是一种"昂贵"的多任务工作方式。而运行于一个进程中的多个线程,它们彼此之间使用相同的地址空间,共享大部分数据,启动一个线程所花费的空间远远小于启动一个进程所花费的空间,而且,线程间彼此切换所需的时间也远远小于进程间切换所需要的时间。当然,在具体的系统上,这个数据可能会有较大的区别;
【2】线程间方便的通信机制,由于同一进程下的线程之间共享数据空间,所以一个线程的数据可以直接为其它线程所用,这不仅快捷,而且方便;
【3】使多CPU系统更加有效。操作系统会保证当线程数不大于CPU数目时,不同的线程运行于不同的CPU上;
【4】改善程序结构。一个既长又复杂的进程可以考虑分为多个线程,成为几个独立或半独立的运行部分,这样的程序会利于理解和修改。
线程的缺点:
【1】调度时, 要保存线程状态,频繁调度, 需要占用大量的机时;
【2】程序设计上容易出错(线程同步问题)。
有人为二者对比专门做过实验,特此转载过来
在Unix上编程采用多线程还是多进程的争执由来已久,这种争执最常见到在C/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。我的环境是我的工作本本:单cpu单核celeron® M 1.5 GZ,1.5G内存,ubuntu10.04 desktop,内核2.6.32。
进程实验代码(fork.c):
#include
#include
#include
#define P_NUMBER 255 /* 并发进程数量 */
#define COUNT 100 /* 每进程打印字符串次数 */
#define TEST_LOGFILE "logFile.log"
FILE *logFile = NULL;
char *s = "hello linux\0";
int main()
{
int i = 0,j = 0;
logFile = fopen(TEST_LOGFILE, "a+"); /* 打开日志文件 */
for(i = 0; i < P_NUMBER; i++)
{
if(fork() == 0) /* 创建子进程,if(fork() == 0){}这段代码是子进程运行区间 */
{
for(j = 0;j < COUNT; j++)
{
printf("[%d]%s\n", j, s); /* 向控制台输出 */
fprintf(logFile,"[%d]%s\n", j, s); /* 向日志文件输出 */
}
exit(0); /* 子进程结束 */
}
}
for(i = 0; i < P_NUMBER; i++) /* 回收子进程 */
{
wait(0);
}
printf("OK\n");
return 0;
}
进程实验代码(thread.c):
#include
#include
#include
#include
#define P_NUMBER 255 /* 并发线程数量 */
#define COUNT 100 /* 每线程打印字符串次数 */
#define Test_Log "logFIle.log"
FILE *logFile = NULL;
char *s = "hello linux\0";
print_hello_linux() /* 线程执行的函数 */
{
int i = 0;
for(i = 0; i < COUNT; i++)
{
printf("[%d]%s\n", i, s); /* 向控制台输出 */
fprintf(logFile, "[%d]%s\n", i, s); /* 向日志文件输出 */
}
pthread_exit(0); /* 线程结束 */
}
int main()
{
int i = 0;
pthread_t pid[P_NUMBER]; /* 线程数组 */
logFile = fopen(Test_Log, "a+"); /* 打开日志文件 */
for(i = 0; i < P_NUMBER; i++)
pthread_create(&pid[i], NULL, (void *)print_hello_linux, NULL); /* 创建线程 */
for(i = 0; i < P_NUMBER; i++)
pthread_join(pid[i],NULL); /* 回收线程 */
printf("OK\n");
return 0;
}
两段程序做的事情是一样的,都是创建“若干”个进程/线程,每个创建出的进程/线程打印“若干”条“hello linux”字符串到控制台和日志文件,两个“若干”由两个宏 P_NUMBER和COUNT分别定义,程序编译指令如下:
~/tmp1$ gcc -o fork fork.c
~/tmp1$ gcc -lpthread -o thread thread.c
实验通过time指令执行两个程序,抄录time输出的挂钟时间(real时间):
time ./fork
time ./thread
每批次的实验通过改动宏 P_NUMBER和COUNT来调整进程/线程数量和打印次数,每批次测试五轮,得到的结果如下:
1) 重复周丽论文实验步骤
进程线程数:255 / 打印次数:100
进程线程数:255 / 打印次数:500
进程线程数:255 / 打印次数:1000
进程线程数:255 / 打印次数:5000
本轮实验是为了和周丽论文作对比,因此将进程/线程数量限制在255个,论文也是测试了255个进程/线程分别进行10 次,50 次,100 次,200 次……900 次打印的用时,论文得出的结果是:任务量较大时,多进程比多线程效率高;而完成的任务量较小时,多线程比多进程要快,重复打印 600 次时,多进程与多线程所耗费的时间相同。
虽然我的实验直到5000打印次数时,多进程才开始领先,但考虑到使用的是NPTL线程库的缘故,从而可以证实了论文的观点。从我的实验数据看,多线程和多进程两组数据非常接近,考虑到数据的提取具有瞬间性,因此可以认为他们的速度是相同的。
当前的网络环境中,我们更看中高并发、高负荷下的性能,纵观前面的实验步骤,最长的实验周期不过1分钟多一点,因此下面的实验将向两个方向延伸,第一,增加并发数量,第二,增加每进程/线程的工作强度。
2)增加并发数量的实验
下面的实验打印次数不变,而进程/线程数量逐渐增加。在实验过程中多线程程序在后三组(线程数500,800,1000)的测试中都出现了“段错误”,出现错误的原因和线程栈的大小有关。
实验中的计算机CPU是32位的赛扬,寻址最大范围是4GB(2的32次方),Linux是按照3GB/1GB的方式来分配内存,其中1GB属于所有进程共享的内核空间,3GB属于用户空间(进程虚拟内存空间),对于进程而言只有一个栈,这个栈可以用尽这3GB空间(计算时需要排除程序文本、数据、 共享库等占用的空间),所以它的大小通常不是问题。但对线程而言每个线程有一个线程栈,这3GB空间会被所有线程栈摊分,线程数量太多时,线程栈累计的大 小将超过进程虚拟内存空间大小,这就是实验中出现的“段错误”的原因。
Linux2.6的默认线程栈大小是8M,可以通过 ulimit -s 命令查看或修改,我们可以计算出线程数的最大上线: (1024102410243) / (10241024*8) = 384,实际数字应该略小与384,因为还要计算程序文本、数据、共享库等占用的空间。在当今的稍显繁忙的WEB服务器上,突破384的并发访问并不是稀罕的事情,要继续下面的实验需要将默认线程栈的大小减小,但这样做有一定的风险,比如线程中的函数分配了大量的自动变量或者函数涉及很深的栈帧(典型的是 递归调用),线程栈就可能不够用了。可以配合使用POSIX.1规定的两个线程属性guardsize和stackaddr来解决线程栈溢出问题, guardsize控制着线程栈末尾之后的一篇内存区域,一旦线程栈在使用中溢出并到达了这片内存,程序可以捕获系统内核发出的告警信号,然后使用 malloc获取另外的内存,并通过stackaddr改变线程栈的位置,以获得额外的栈空间,这个动态扩展栈空间办法需要手工编程,而且非常麻烦。
有两种方法可以改变线程栈的大小,使用 ulimit -s 命令改变系统默认线程栈的大小,或者在代码中创建线程时通过pthread_attr_setstacksize函数改变栈尺寸,在实验中使用的是第一 种,在程序运行前先执行ulimit指令将默认线程栈大小改为1M:
~/tmp1$ ulimit -s 1024
~/tmp1$ time ./thread
进程线程数:100 / 打印次数:1000
进程线程数:255 / 打印次数:1000 (这里使用了第一次的实验数据)
进程线程数:350 / 打印次数:1000
进程线程数:500 / 打印次数:1000 (线程栈大小更改为1M)
进程线程数:800 / 打印次数:1000 (线程栈大小更改为1M)
进程线程数:1000 / 打印次数:1000 (线程栈大小更改为1M)
**3)增加每进程/线程的工作强度的实验 **
这次将程序打印数据增大,原来打印字符串为:
char *s = “hello linux\0”;
现在修改为每次打印256个字节数据:
char *s = “1234567890abcdef
1234567890abcdef
1234567890abcdef
1234567890abcdef
1234567890abcdef
1234567890abcdef
1234567890abcdef
1234567890abcdef
1234567890abcdef
1234567890abcdef
1234567890abcdef
1234567890abcdef
1234567890abcdef
1234567890abcdef
1234567890abcdef
1234567890abcdef\0”;
进程线程数:255 / 打印次数:100
进程线程数:255 / 打印次数:500
进程线程数:255 / 打印次数:2000 (实验太耗时,因此只进行了2轮比对)
【实验结论】
从上面的实验比对结果看,即使Linux2.6使用了新的NPTL线程库(据说比原线程库性能提高了很多,唉,又是据说!),多线程比较多进程在效率上没有任何的优势,在线程数增大时多线程程序还出现了运行错误,实验可以得出下面的结论:
在Linux2.6上,多线程并不比多进程速度快,考虑到线程栈的问题,多进程在并发上有优势。
4)多进程和多线程在创建和销毁上的效率比较
预先创建进程或线程可以节省进程或线程的创建、销毁时间,在实际的应用中很多程序使用了这样的策略,比如Apapche预先创建进程、Tomcat 预先创建线程,通常叫做进程池或线程池。在大部分人的概念中,进程或线程的创建、销毁是比较耗时的,在stevesn的著作《Unix网络编程》中有这样 的对比图(第一卷 第三版 30章 客户/服务器程序设计范式):
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倍,原因有两个:
#include
#include
#include
#include
#include
#include
#include
#include
int count; /* 子进程创建成功数量 */
int fcount; /* 子进程创建失败数量 */
int scount; /* 子进程回收数量 */
/* 信号处理函数–子进程关闭收集 */
void sig_chld(int signo)
{
pid_t chldpid; /* 子进程id */
int stat; /* 子进程的终止状态 */
/* 子进程回收,避免出现僵尸进程 */
while ((chldpid = wait(&stat)) > 0)
{
scount++;
}
}
int main()
{
/* 注册子进程回收信号处理函数 */
signal(SIGCHLD, sig_chld);
int i;
for (i = 0; i < 100000; i++) //fork()10万个子进程
{
pid_t pid = fork();
if (pid == -1) //子进程创建失败
{
fcount++;
}
else if (pid > 0) //子进程创建成功
{
count++;
}
else if (pid == 0) //子进程执行过程
{
exit(0);
}
}
printf("count: %d fcount: %d scount: %d\n", count, fcount, scount);
}
创建10万个线程(pthreadcreat.c):
#include
#include
int count = 0; /* 成功创建线程数量 */
void thread(void)
{
/* 线程啥也不做 */
}
int main(void)
{
pthread_t id; /* 线程id */
int i,ret;
for (i = 0; i < 100000; i++) /* 创建10万个线程 */
{
ret = pthread_create(&id, NULL, (void *)thread, NULL);
if(ret != 0)
{
printf ("Create pthread error!\n");
return (1);
}
count++;
pthread_join(id, NULL);
}
printf("count: %d\n", count);
}
在我的赛扬1.5G的CPU上测试结果如下(仍采用测试5次后计算平均值):
从数据可以看出,多线程比多进程在效率上有5~6倍的优势,但不能让我们在使用那种并发模式上定性,这让我想起多年前政治课上的一个场景:在讲到优 越性时,面对着几个对此发表质疑评论的调皮男生,我们的政治老师发表了高见,“不能只横向地和当今的发达国家比,你应该纵向地和过去中国几十年的发展历史 比”。政治老师的话套用在当前简直就是真理,我们看看,即使是在赛扬CPU上,创建、销毁进程/线程的速度都是空前的,可以说是有质的飞跃的,平均创建销 毁一个进程的速度是0.18毫秒,对于当前服务器几百、几千的并发量,还有预先派生子进程/线程的必要吗?
预先派生子进程/线程比现场创建子进程/线程要复杂很多,不仅要对池中进程/线程数量进行动态管理,还要解决多进程/多线程对accept的“抢” 问题,在stevens的测试程序中,使用了“惊群”和“锁”技术。即使stevens的数据表格中,预先派生线程也不见得比现场创建线程快,在 《Unix网络编程》第三版中,新作者参照stevens的测试也提供了一组数据,在这组数据中,现场创建线程模式比预先派生线程模式已有了效率上的优 势。因此我对这一节实验下的结论是:
预先派生进程/线程的模式(进程池、线程池)技术,不仅复杂,在效率上也无优势,在新的应用中可以放心大胆地为客户连接请求去现场创建进程和线程。
点击进入