“过好每一天,就是对无奈人生最好的报复。” --朱德庸
在了解完Linux中信号的内容之后,我们接下来对多线程内容进行讲述,这一模块内容较长,请耐心。
对于多线程内容,我们将从接下来这几部分内容来讲述:线程概念,线程控制,线程安全和线程应用。
目录
1.线程概念
1.1内容
1.2线程和进程
1.3多线程在同一进程的执行特征
2.线程控制
2.1线程创建
2.2线程终止
2.2.1线程入口函数return
2.2.2退出当前线程
2.2.3退出指定线程
2.3线程等待与分离
2.3.1线程等待
2.3.1线程分离
首先我们需要清楚的基本概念是,线程是进程中的一个执行流程,是CPU进程调度执行的基本单元,调度一段代码便是通过线程来完成的。(进程是系统分配资源的基本单元)
而在Linux中pcb是程序运行过程中的描述,因此在Linux下的线程是通过pcb来实现的。
则多进程和多线程的理解大致应该是:当我们需要完成一个多任务目标项时,多进程是编写多个程序对应多个任务,然后每个程序对应的pcb来调度执行各自的任务;而多线程是对于同一个程序而言,将程序内容划分成多个模块来对应多个任务,然后创建每个模块的pcb来调度各自的任务(Linux中一个进程允许有多个pcb)。
那么对于多进程而言,占用系统资源更多(创建了多个进程),但是运行稳定(进程之间相互独立,互不影响);对于多线程而言,占用系统资源较少(将一个程序划分多个模块),但是运行容易崩溃,书写代码需多加注意(同一个程序中:一荣俱荣,一损俱损)。
最后读者引用以下apue中对于线程的讲述:
我们可以很明显看出,在Linux中线程功能便是将一个进程设计成为同一时间内可完成多个任务。也就是上述内容中笔者所提到的:将同一个程序设计分为多个模块,每个模块对应不同任务。
我们再来总结一下,进程和线程的区别:
对于多进程:稳定,不易崩溃;
对于多线程:线程间通信更加灵活(共享虚拟地址空);创建和销毁成本更低(线程之间很多资源共享,单独创建内容较少);同进程的线程调度成本更低(CPU上的快表信息,页表指针……都不需要替换)。
那么,当对程序的安全性和稳定性要求大于资源和性能要求时,我们便需要使用多进程来构建;反之,我们使用多线程。
多线程是设计在同一个进程之中来完成不同的任务,那么为了避免线程执行不混乱,我们必须明确线程在进程之中的特征对其加以约束和控制。
首先从具体实现上而言,每个线程调度执行的就是一个函数。当我们市容vfork接口创建子进程之后,父子进程公用同一块虚拟地址空间,为了避免运行时可能出现的栈混乱,因此父进程会阻塞直到子进程替换或退出。
然后当多进程之间出现混乱时,我们可以将所有可能混乱的地方,给每个线程都拷贝一份,即手动实现线程之间的相互独立。
最后线程之间的独有信息有:标识符,栈,上下文数据,信号屏蔽字和errno……线程之间的共享信息有:虚拟地址空间,文件描述符,信号处理方式,工作路径,用户ID,组ID……
线程的控制我们将从创建,终止,等待和分离这四部分讲述。不过在了解线程控制之前,我们需要了解的是,Linux中并不存在线程的直接内容,线程是我们在上层提出的概念,而Linux操作系统并没有单独的内容来对其进行实现。
也正如我们线程概念中提到的内容,Linux中的线程对应的是底层中的轻量级pcb,即lwp。所以Linux操作系统并未直接提供相应的系统调用接口供我们来实现线程控制,我们有关线程控制的所有操作都是前人封装而成的库函数。
int pthread_create(pthread_t *tid, pthread_attr *attr, void* (*routine)(void*), void *arg);
pthread_create函数的作用:创建一个线程,指定这个线程需要运行的函数routine,并且给该函数传入一个数据arg。
其中,tid:传入一个pthread_t类型变量的空间地址,用于接收线程ID--线程的操作句柄;attr:线程属性,通常制NULL; routine:函数指针,传入线程入口的函数地址,该线程调度执行的便是此函数;arg:给routine线程入口函数传入的参数。
返回值:创建线程成功则返回0,失败则返回非0值(错误编号)。
按照以往习惯,我们来通过书写代码进行一个说明:
书写完成之后,我们进行执行:
会报错,但是对于代码中的头文件pthread.h我们已经添加,为什么还会产生在mian中pthread_create未定义的情况。这是因为pthread的内容是我们连接第三方库来实现的,所以我们在执行中需要在gcc pcreate.c -o pcreate后加上-lpthread。
再次执行:
就不会产生错误啦~。而且我们能很直观看出二者好似在交替运行,但实际上并非如此,对于线程执行的优先级是由CPU调度决定了。这是我们在进程概念中提到的内容:CPU分时机制,即CPU的调度是通过时间片的分配来决定的。
所以当我们取消sleep之后,我们可以发现在线程自己的时间片之中,自身的内容会实现很多次。(结果太长,不便于展示,望读者自己动手)
线程的终止即是退出一个线程的运行,线程调度运行的是创建时所传入的routine函数,所以当routine函数退出后,线程也会退出。
线程终止的三种方式:
当入口函数被return退出之后,对应的线程也便结束。不过需要注意的是:main中return退出的会是主进程,而不仅是主线程。
void pthread_exit(void* retval);
pthread_exit接口用于退出当前线程,可在线程的任意位置被调用,其中retval用于设计线程退出的返回值。
int pthread_cancel(pthread_t tid);
pthread_cancel接口用于退出指定进程,可在线程的任意位置被调用,其中tid便是会被退出的线程标识。注意被退出的线程其返回值会不可控,不能用其原本的退出返回值来判断。
在了解线程等待与分离之前,我们需要了解两部分知识点。首先便是,当我们使用线程退出接口退出主线程后,我们可以发现其他我创建得到的线程依旧会照常运行。如下图展示:
并且在程序运行中,我们查看进程信息:
我们可以发现,当我们设计主线程退出时,其资源并没有被完全释放,而其是一种do_exit状态。
那么第二点我们需要了解的内容便是,线程退出之后,该线程的资源也并不会立即完全被释放,因为我们需要保存其返回值。(值得注意的是:所有的线程退出之后,则进程退出释放资源;当进程打断进行退出,则会想退出所有的线程。)
了解完上述内容之后,我们来讲述线程等待和分离的内容:
线程等待便是:等待指定的线程退出,获取线程的返回值,回收退出线程的所有资源。
int pthread_join(pthread_t tid, void **retval);
pthread_join接口用于线程等待,用于阻塞线程,等待tid线程执行完毕后,再执行接下来的线程。
其中,tid便是:要等待退出的指定线程tid;retval:用于获取线程的退出返回值。(因为线程退出返回值时void*类型,因此需要传入指针变量的地址,即二级指针)
返回值:成功返回0,失败返回非0(错误编号)。
通过代码来展示:
执行结果为:
可以很直观的看出,待entry线程运行结束之后,main主线程才开始执行。(真正操作时,大家需要格外注意线程中变量的生命周期,防止局部变量走出函数被释放其余线程访问错误情况。)
在线程属性中,存在一个格外的属性称作分离属性,默认值是joinable状态,表示线程退出之后,不会自动释放资源,需要被其他线程等待。
但是有些时候我们并不关心某个线程的返回值,更不期望等待它的退出,则此时我们可以将其状态设置为detach状态。表示该线程退出后,其资源会立即被释放,不需要被等待。
int pthread_detach(pthread_t tid);
pthread_detach接口便用于设计对应的线程属性为detach属性。其中,tid则是设计分离属性的detach的线程。
返回值:成功返回0,失败返回非0(错误编号)。