关于多进程和多线程,一直想写点什么来进行一次总结,今天终于提笔了,若有讲解错误之处,希望广大读者能给予指正。,我想从以下几个方面进行一次详解划分.第一,运用。第二,同步。第三,通信。第四,选择。
那么闲话少说,开始第一个,关于线程和进程之间的运用。
什么是进程?
有一个很官方的说法:进程是程序在计算机上的一次执行活动。但我觉得,可能这句话有点不对,应该换成进程加线程是程序在计算机上的一次执行活动才更加的合理。因为进程是资源分配的最小单位,线程是CPU调度的最小单位。所以我们看到进程的时候,应该联想到资源,看到线程的时候联想到程序的执行,程序真正再跑的只是线程,例如程序开始的时候,就是一个线程利用进程的资源再跑。
打印出进程ID和主线程ID。
如何创建进程?
进程的创建,我们最容易想象到的两个函数是fork和vfork。
fork在创建的子进程复制了父进程的资源,新旧进程使用同一个代码段,复制了数据段和堆栈段,但是当进程开始运行的时候,新旧的地址空间在物理内存上开始划分开来,这里使用了copy_on_write技术来达到真正意义上的分开。两者独立运行,也就会在数据段和堆栈段上的数据不会有冲突。fork的优点取决父子进程完全独立,这样具有很好的并发性。
vfork在创建子进程的时候子进程是完全运行在父进程的地址空间上,使用vfork创建的子进程,笔者在这强烈建议使用exec函数族,因为使用exec函数族,执行了另一个程序,子进程不会对父进程的地址空间有任何引用,若需求上面不能执行exec函数族,那么子进程必须使用_exit()函数进行退出,也不能是执行exit()函数,因为exit函数会将标准输入输出流进行刷新,释放所占有的资源以及清空缓冲区,而_exit()没有刷新缓冲区功能,这里是考虑到vfork父子进程使用同一个内存空间,不然程序会出现意料之外的结果,比如如下图所示程序就出现三个进程,两个子进程和一个父进程,只有当子进程执行了_exit(),exec函数族等函数的时候,父进程才会正常执行。所以在此建议读者,如果需要使用exec函数族的时候,就用vfork()创建,不然就用fork()创建子进程.
运行结果是
num=0
子进程pid=29356
num=7917675
父进程pid=29355
num=0
子进程pid=29360
进程的等待,父进程可以用
wait和waitpid函数等待子进程的结束,wait是等待 任意一个子进程结束,waitpid 可以对等待的方式进行设置。如果没有
子进程,直接返回。如果没有结束的子进程, 父进程将被 阻塞(waitpid可以设置非阻塞)。wait、waitpid都可以取得 子进程的结束状态。pid_t
wait(int *status),返回结束的子进程id,参数用于带出结束状态,pid_t waitpid(pid_t pid,int* status,int
options),返回结束的子进程id,参数status用于 带出结束状态,options可以指定非阻塞,一般用0代表阻塞。结束状态 status可以用
宏做一些判断:WIFEXITED(status) 可以判断子进程是否正常结束。
#include<pthread.h> #include<stdio.h> #include<stdlib.h> #include<signal.h> int main(int argc,char* argv[]) { int status=0; pid_t pid=fork(); if(pid<0) { perror("fork"); exit(1); } else if(pid==0) { sleep(1);//让进程进行休眠状态,保证父进程先执行 printf("子进程pid=%d\n",getpid()); exit(5); } else { wait(&status);//程序进行阻塞状态,看其是否是等待子进程执行完毕 printf("父进程pid=%d,%d\n",getpid(),status); if(WIFEXITED(status)) { printf("the child process exit normally\n"); } else { printf("the child process exit abnormally\n"); } } } <span style="font-family:KaiTi_GB2312;"><span style="font-size:18px;"> </span></span>
进程的结束,进程的结束可以分程序执行完毕,自然结束,也可以由int kill(pid_t pid, int sig);向指定的进程号发送信号,如果发送9信号,默认是杀掉指定的进程,因为考虑到资源问题,担心一些动态内存在进程里面申请后,正准备释放之前该进程被其它进程kill了,导致出现内存泄露,所以我们尽量避免使用kill这样暴力的函数
父子进程之间的执行,注在线程里面没有父子线程关系,只有主线程和其它线程.父进程启动子进程后,父子进程 同时运行。如果子进程先结束,子进程
给父进程发信号,父进程 负责回收子进程的资源。 父进程启动子进程后,父子进程 同时运行。如果父进程先结束,子进程成 孤儿进程,子进程 认
init进程(进程1)做新的父进程,init进程 也叫 孤儿院。父进程启动子进程后,父子进程 同时运行。如果子进程先结束,子进程
给父进程发信号,但父进程有可能没收到信号,或者收到了没处理,子进程 会变成 僵尸进程
线程的创建,使用函数int
pthread_create(pthread_t *tidp,
const
pthread_attr_t *attr,
(
void
*)(*start_rtn)(
void
*),
void
*arg);第一个参数是指向线程标示符的指针,线程标示符,一个无符号的长整型的类型,在头文件里面我们可以看到它的定义typedef
unsigned long int
pthread_t;第二个参数是指向线程属性的指针,这个如若不设置属性,那么设置为空,一切就以默认属性,笔者觉得这个属性比较重要,将在后面详细讲解.第三个参数,顾名思义是一个函数指针,函数的返回值类型是无符号的指针类型,第四个参数是函数指针函数的传递参数,如若没有参数,则可以设置空,如果需要传递多个参数,可定义结构体进行传递,不过传递的时候要强制转换成void*类型。
线程的等待,使用函数pthread_join(),该函数是当线程结束的时候,清收线程的资源,然后函数返回,这里可以保证不会存在主线程结束了,开启的线程还么开始执行,或者只是执行了一半,有读者可能会有疑问,不是说线程是执行单位,进程才是资源单位,为什么还有清理资源工作,这里我们需要了解到其实在Linux中,新建的线程并不是在原先的进程中,而是系统通过一个系统调用函数clone()。该系统调用copy了一个和原先进程完全一样的进程,并在这个进程中执行线程函数。不过这个copy过程和fork不一样。
copy后的进程和原先的进程共享了所有的变量(有点像是vfork的功能),运行环境。这样既可以做到多线程之间共享全局变量。所以pthread_join函数保证了线程的全部执行和资源清理工作。
线程的属性,也就是在创建线程函数pthread_create里面的第三个参数pthread_attr_t
*attr,首先我们要知道,pthread_attr_t是一个结构体类型,如下所示
typedef
struct
{
int detachstate; 线程的分离状态
int schedpolicy; 线程调度策略
struct sched_param schedparam; 线程的调度参数
int inheritsched; 线程的继承性
int scope; 线程的作用域
size_t guardsize; 线程栈末尾的警戒缓冲区大小
int stackaddr_set;
void
* stackaddr; 线程栈的位置
size_t stacksize; 线程栈的大小
}pthread_attr_t;
属性pthread_attr_t主要包括scope属性、detach属性、堆栈地址、堆栈大小、优先级。在这里就注重讲解堆栈大小属性的设置及其应用,至于其它属性值,读者若是有兴趣,可以参考http://blog.csdn.net/zsf8701/article/details/7843837,这篇博客.在很多的时候,我们需要增加并发数量来完成某一项认为,特别是在网路编程里面服务端程序上支持多并发处理,我们需要更多的并发量去支持,我们假设使用的计算机CPU是32位数,最大的寻址范围是4GB,1GB是所有的进程共享的内核空间,3GB是用户空间,也就是进程虚拟内存空间,我们在linux下面用ulimit
-s 可以产看当前系统上线程的堆栈空间大小是多少,一般默认是8M,这样我们可以计算出线程的最大上限数量是: (1024*1024*1024*3) /
(1024*1024*8) =
384,实际数字应该略小384,因为还要计算程序文本、数据、共享库等占用的空间。当前的WEB服务器上面并发量大于384已经是很平常的事了,那么我们如何突破这个限制,已达到更多的并发量呢,有两种办法,第一是通过ulimit
-s 去修改堆栈的空间大小,比如 ulimit -s 1024
每个线程只分配1M的空间,那么并发量就可以增加到384*8,可以根据自己的项目需求去更改这个限制,但是这样做的缺点是修改了,那么所有在本机器上面跑的进程,都会默认这个设置,就会让一些进程因为堆栈空间太小导致段错误,所以显然这个不是一个很理智的办法。于是我们就有了线程设置堆栈大小的属性,保证本次受影响的线程只是设置这个属性的线程。说了这么多,我们就开始用实际代码练练手。
#include<stdio.h> #include<string.h> #include<limits.h> #include<pthread.h> void* fun(void* i); int main(int argc,char* argv[]) { pthread_t t[1000]; pthread_attr_t attr; int i=0; if(pthread_attr_init(&attr))//初始化一个属性 { perror("attr"); } if(pthread_attr_setstacksize(&attr,163840))//设置线程的堆栈大小,我们开始利用初始化的属性进行堆栈空间大小的设置,这里是以字节为单位的堆栈空间大小的调整 { perror("attr\n"); } for(i=0;i<1000;i++) { pthread_create(&t[i],&attr,fun,NULL);//开启一个线程 } for(i=0;i<1000;i++) { pthread_join(t[i],NULL); } printf("%d\n",PTHREAD_STACK_MIN);//这里需要牵扯到一个知识点,我们在设置堆栈空间大小的时候,不能小于linux内部定义的最小堆栈大小,可以打印这个宏看看是多少,根据操作系统不同而不同,我这里是16384 } void* fun(void*i) { }
在设置属性值的时候,注意点是第一先利用pthread_attr_init初始化一个属性,后续才能利用这个初始化好的属性值。
在设置堆栈大小上,不得不提到一个重要的属性是线程栈末尾的警戒缓冲区,int
pthread_attr_setguardsize(pthread_attr_t *attr, size_t
guardsize);设置线程的堆栈保护区,第二个参数设置堆栈的保护区,一般默认是4096个字节,一旦线程栈在使用中溢出并到达了这片内存,程序可以捕获系统内核发出的告警信号,然后使用
malloc获取另外的内存,并通过stackaddr改变线程栈的位置,以获得额外的栈空间,这个动态扩展栈空间办法需要手工编程。
线程的结束,我们在启动一个线程,强烈建议不要在外部用pthread_kill,pthread_cancel等函数强行的中端一个线程,这样容易导致很多问题,因为这样的强行取消我们不清楚线程终止的地方会是在哪里,可能会在这之前动态申请了内存,而没有得到释放,更严重的是在刚进入一个加锁代码中,然后被强制退出,那么就会导致死锁现象,所以建议让线程能够自己执行完,让所有的资源能够得到释放,若必须使用外部干扰,那么我们需要在线程里面设置安全的取消点,也就是当线程接收到cancel的时候,不会马上退出,运行到一个相对比较安全的地方,然后再退出本次线程,设置取消点上,我们需要让本次线程能够做到延迟取消 pthread_setcancelstate(PTHREAD_CANCEL_DISABLE,NULL);在线程设置这个属性,然后设置一个取消点,取消点有很多的线程函数都可以,常用的是pthread_testcancel(),pthread_join()等
第二个,同步