再次学习之多进程
基本概念的再次学习
- 线程是程序执行的最小单位(进程是资源管理的最小单位),线程隶属于某个进程中
进程有自己的数据段、代码段和堆栈段。线程通常叫做轻型的进程,每个线程共享其所附属进程的所有资源,包括打开的文件、内存页面、信号标识及动态分配的内存等
- 线程和进程比起来很小,因此线程花费更少的CPU资源
- 在大并发、不断有客户端在请求服务器端的情况下,服务端则应该使用线程
当进程退出时该进程所产生的线程都会被强制退出并清除。一个进程至少需要一个线程(主线程)作为它的指令执行体,进程管理着资源,将线程分配到某个CPU执行
- 线程的分类
- 用户级线程(由用户决定,主要解决上下文切换的问题)和内核级线程(由内核调度机制实现)
- 用户级线程绑定内核级线程运行,一个进程中的内核级线程会分配到固定的时间片,用户级线程分配的时间片以内核级线程为准
当CPU分配给线程的时间片用完后但线程没有执行完毕,此时线程会从运行状态返回到就绪状态,将CPU让给其它线程使用
在Linux中,一般采用pthread线程库实现线程的访问与控制,因此在编译的时候要链接库pthread,即-lpthread
- 线程的标识
- 每个线程都有自己的唯一标识(标识是pthread_t数据类型)
- 线程标识只在所属的进程环境中有效
++与线程标识相关的函数++:
int pthread_equal(pthread_t,pthread_t)
:判断线程是否一致;pthread_t pthread_self(void)
:获得调用线程自己的ID
学习时遇到的理解问题
- 问题1:对于CPU而言,每次均有一个进程抢到CPU,而进程的运行实质上是指进程中的线程在运行。但是一个进程中有多个线程(包括主线程和主线程创建的若干个为了满足客户需求的子线程),那么在某一时刻,到底是哪个线程获得CPU在运行呢?
- 根据网上的辅助资料显示,具体哪个线程得到CPU并执行需要通过系统的调度,例如根据这些线程的优先级将某个线程从就绪状态调度为running状态。即系统是先把某个进程调度为running状态,后再根据某些规则(例如操作系统课上提到的优先级和调度算法)将某个线程调度为running状态。(当然这些线程之间也会根据某个算法进行切换,在进程拥有很多线程的时候,一般而言某个线程很难一直占有CPU)
- 问题2:用户级线程与内核级线程是处于绑定运行的状态,也就是说用户级线程跟在内核级线程后进行运行。那么不会出现以下这个情况吗:某一个内核级线程与多个用户级线程绑定在一起,当这个内核级线程被调度的时候,与它所绑定的多个用户级线程也就被调度了,这样不会造成CPU的疑惑吗?(就是CPU怎么知道要执行哪个线程呢)
- 默认情况下用户级线程和内核级线程是一对一,例如一个内核级线程被允许执行10ns,那么与它所绑定的这个用户级线程也就执行10ns的时间。虽然也可以多对一,但会导致实时性比较差
线程的创建
- 调用函数:
int pthread_create(pthread_t *restrict tidp,const pthread_attr_t *restrict attr,void *(*start_rtn)(void*),void *restrict arg);
成功则返回0 - tidp:线程标识符指针
- attr:线程属性指针(不要求则传空)
- start_rtn:线程运行函数的起始地址,(需要用户自己定义)
- arg:传递给线程运行函数的参数
- 新创建线程从start_rtn函数的地址开始运行
不能保证新线程和调用线程的执行顺序
实践环节
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
void* th_fn(void *arg)
{
int distance=(int)arg;
int i;
for(i=1;i<=distance;i++){
printf("%lx run %d\n",pthread_self(),i);
sleep(1);
}
return (void*)0;
}
int main(void)
{
int err;
pthread_t rabbit,turtle;
if((err = pthread_create(&rabbit,NULL,th_fn,(void*)50)) != 0){
printf("error!");
}
if((err = pthread_create(&turtle,NULL,th_fn,(void*)50)) != 0){
printf("error!");
}
printf("control thread id:%lx\n",pthread_self());
printf("finish!\n");
return 0;
}
运行结果如下所示:
从输出可以看到,程序并没有创建子线程成功,这个原因主要是因为当子线程被创建时,无法判断是主线程先执行还是子线程先执行,从这个结果可以看到,程序在创建完子线程后执行了主线程,因此当主线程执行完毕后,因为遇到了return 0,代表了整个进程的结束,因此新创建的子进程也被强制退出了。
由此可以得知若要令子线程也被执行,则应令主线程处于等待状态,即令主线程等待子线程执行完毕后再退出。在代码上只需在return 0;语句前加上sleep(10)语句,令主线程处于阻塞状态即可。
由运行结果可以看出,两个线程正在交替运行,运行结束后才执行主线程并退出
- 问题3:从上面这个实践可以看出,在调用pthread_create()创建子线程的时候,是通过第四个参数进行参数传递,但是实际应用中可能向子线程传递多个参数,这个时候该怎么办呢?
把所有要传的参数组成一个结构体,把这个结构体传给子线程以完成向子线程传递多个参数
- 关于问题3的实践
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
typedef struct
{
char name[20];
int time;
int start;
int end;
}RaceArg;
void* th_fn(void *arg)
{
RaceArg *r = (RaceArg*)arg;
int i = r->start;
for(; i <= r->end ; i++){
printf("%s(%lx) running %d\n",r->name,pthread_self(),i);
usleep(r->time);
}
return (void*)0;
}
int main()
{
int err;
pthread_t rabbit,turtle;
RaceArg r_a = {"rabbit",10,20,50};
RaceArg t_a = {"turtle",20,10,60};
if((err = pthread_create(&rabbit,NULL,th_fn,(void*)&r_a)) != 0){
printf("error!");
}
if((err = pthread_create(&turtle,NULL,th_fn,(void*)&t_a)) != 0){
printf("error!");
}
pthread_join(rabbit,NULL);
pthread_join(turtle,NULL);
printf("control thread id:%lx\n",pthread_self());
printf("finish!\n");
return 0;
}
即定义结构体,把需要传的参数放在结构体内,然后把指向结构体的指针传到子线程中。
- 问题4:从上面两次的实践可以知道,通过线程创建函数创建了2个子线程,这些子线程在内存中是如何存储的,以保证他们内部存储的信息互不相干扰又能共同共享同一个进程?
- 线程所拥有的局部变量是互不相干扰的,以上面实践的为例,在内存中的存储方式如下图所示:
但对于全局变量则是存储在进程数据段当中,子线程则共享数据段中的数据(共享资源),但这样好像会对安全性有一定的影响
线程的终止
- 主动终止
- 线程的执行函数中调用return语句
调用pthread_exit()
- 被动终止
被同一进程的其他线程取消:pthread_cancel(pthid),pthid为被终止的线程标识符。此函数类似进程中的kill函数
注意,若在线程中调用exit(0)这类函数时,则是进程被终止了。另外,当进程还未结束时,退出线程所占用的资源并不会随线程结束而释放
- pthread_join()函数
- 原型为
int pthread_join(pthread_t th,void **thread_return);
th为被等待线程的标识符,thread_return为用户定义指针,用来存储被等待线程的返回值。成功则返回0 调用pthread_join对资源的释放也有一定帮助,避免内存过于拥挤
对于pthread_join()函数,我认为难点主要是第二个参数,它是一个二级指针变量,为了更好理解pthread_join()函数的第二个参数,可以使用下面这个例子(代码功能是将传入子线程的两个参数相加)
#include <pthread.h>
#include <stdlib.h>
#include <stdio.h>
typedef struct
{
int d1;
int d2;
}Arg;
void* th_fn(void *arg)
{
Arg *r = (Arg*)arg;
return (void*)(r->d1 + r->d2);
}
int main(void)
{
int err;
pthread_t th;
Arg r = {20,50};
if((err = pthread_create(&th,NULL,th_fn,(void*)&r)) != 0){
printf("error!");}
int *result;
pthread_join(th,(void**)&result);
printf("result is %d\n",(int)result);
return 0;
}
运行结果如下:
- 类似地,如果希望pthread_join()函数能返回多个参数,就把这多个参数组成一个结构体并令线程返回指向该结构体的指针变量即可。
线程的清理和控制
- pthread_cleanup_push(void (rtn)(void ),void* arg)函数
- pthread_cleanup_pop(int execute)函数,成功则返回0
- 两者成对出现,即
while(execute){
执行线程处理函数
}
- 触发线程调用清理函数的动作
- 调用pthread_exit
- 响应取消请求
用非零execute参数调用pthread_cleanup_pop时
实践
#include <pthread.h>
#include <stdlib.h>
#include <stdio.h>
void clean_fun(void *arg)
{
char *s = (char*)arg;
printf("clean_func:%s\n",s);
}
void* th_fun(void *arg)
{
int execute = (int)arg;
pthread_cleanup_push(clean_fun,"first clean");
pthread_cleanup_push(clean_fun,"second clean");
printf("thread running %lx\n",pthread_self());
pthread_cleanup_pop(execute);
pthread_cleanup_pop(execute);
return (void*)0;
}
int main()
{
int err;
pthread_t th1,th2;
if((err = pthread_create(&th1,NULL,th_fun,(void*)1)) != 0){
printf("error");
}
pthread_join(th1,NULL);
printf("th1(%lx) finished\n",th1);
if((err = pthread_create(&th2,NULL,th_fun,(void*)1)) != 0){
printf("error");
}
pthread_join(th2,NULL);
printf("th2(%lx) finished\n",th2);
return 0;
}
由运行结果也可以看出,栈的特点是后入先出,即先压入栈的先被处理。
进程和线程启动和终止方式的比较
线程的状态转换
<未完>