线程概念
基本概念
- 概念:借助进程理解线程:
- 线程是进程中的一条执行流程,在 Linux 下线程是由 pcb 来实现的,相较于我们之前所学的进程 pcb 而言,线程 pcb 更加轻量化,因此 Linux 下的线程被称之为轻量级进程;
- 一切进程至少都有一个执行线程,这些线程在进程地址空间内运行,共享了进程的大部分资源,将进程的资源合理分配给每个执行流程,就形成了线程执行流;
- 图片解析:
多线程优缺点
- 多线程优点:
- 线程间通信更加灵活,进程间通信方式都可以使用,并且还可以使用函数、全局变量等等;
- 创建一个新线程的代价要比创建一个新进程小得多;
- 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多;
- 线程占用的资源要比进程少很多;
- 能充分利用多处理器,增加执行流并行数量;
- 在等待慢速 I/O 操作结束的同时,程序可执行其他的计算任务;
- 计算密集型应用为进行大量数据运算的应用,处理该应用时,为了能在多处理器系统上运行,将计算分解到多个线程中实现;
- I/O密集型应用为进行大量 I/O 操作的应用,处理该应用时,为了提高性能,将 I/O 操作重叠执行,使用多线程执行,可以同时等待不同的 I/O 操作;
- 多线程缺点:
- 性能损失:一个很少被外部事件阻塞的计算密集型线程往往无法与其它线程共享同一个处理器,如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变;
- 健壮性降低:编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的;
- 缺乏访问控制:进程是访问控制的基本粒度,在一个线程中调用某些 OS 函数会对整个进程造成影响;
- 编程难度提高:编写与调试一个多线程程序比单线程程序困难得多;
- 异常问题:线程是进程的执行分支,单个线程如果出现问题,会触发信号机制,终止进程,而进程一旦被终止,该进程内的所有线程都会被终止;
多线程用途
- 合理的使用多线程,能提高 CPU 密集型程序的执行效率;
- 合理的使用多线程,能提高 I/O 密集型程序的用户体验(如生活中我们一边写代码一边下载开发工具,就是多线程运行的一种表现);
Linux 进程 VS 线程
进程与线程
- 进程是系统进行资源分配的基本单元;
- 线程是系统进行流程调度的基本单元;
- 线程共享进程数据,但是也拥有自己独有的一部分数据:
- 线程 tid;
- 一组寄存器;
- 栈区;
error
错误码;- 信号屏蔽字;
- 调度优先级;
进程中多线程的共享
- 多个线程使用同一地址空间,因此 Text Segment、Data Segment 都是共享的,如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境:
- 文件描述符表;
- 每种信号的处理方式 (SIG_ IGN、SIG_ DFL 等或者自定义的信号处理函数);
- 当前工作目录;
- 用户 id 和组 id;
进程与线程关系图
线程控制
编译问题
- 与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以
pthread_
打头的,但是在 Linux 中并没有向上提供用于创建线程的接口,因此一些大佬对系统调用接口进行封装实现了上层用户态的线程控制接口,所以在使用到这些函数时就需要进行以下操作:- 要使用这些函数,就需要包含头文件
<pthread.h>
; - 在编译时需要链接这些线程函数库,使用编译器命令的
-lpthread
选项,指明链接的文件名称;
- 要使用这些函数,就需要包含头文件
创建线程
线程创建函数
int pthread_create(pthread_t* tid, const pthread_attr_t* attr, void* (*start_routine)(void*), void* arg);
- 函数参数:
tid
:输出型参数,传入一个整型变量,用来接收被创建线程的 tid;- tid:线程创建好之后会在虚拟内存中又有一块自己独有的空间,该值就是这块空间的首地址;
attr
:设置线程属性,一般不需要我们自己设置,所以置为 NULL;start_routine
:函数指针,表示创建出的这个线程要去执行的函数流程,这个函数是一个参数为void*
且返回值也为void*
的函数;arg
:传给线程所执行函数的参数,也就是上一个参数中函数指针所指向函数的参数,如果不传则置为 NULL;
- 返回值:成功返回 0,失败返回错误码;
- 错误检查:
- 传统的一些函数是,成功返回 0,失败返回 -1,并且对全局变量
errno
赋值以指示错误;而pthread_create
函数出错时不会设置全局变量errno
,而是将错误码通过返回值返回; pthread_create
创建的线程是拥有自己独有的errno
变量的,以支持其使用errno
的代码,对于pthread_create
函数的错误,建议通过返回值来判定,因为读取返回值要比读取线程内的errno
变量的开销更小;
- 传统的一些函数是,成功返回 0,失败返回 -1,并且对全局变量
- 举例:在一个进程中,我们在主函数中进行死循环打印,创建一个线程也执行死循环打印流程,以我们之前的认知,如果程序陷入到死循环的话,那么就不会在执行其他代码,但是该程序结果为两个死循环都在执行,这就是多线程执行流;
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <pthread.h>
//创建出的线程所执行的流程,打印输出内容
void *rout(void *arg) {
while(1) {
printf("i am thread!\n");
sleep(1);
}
}
int main( void ){
//线程 tid
pthread_t tid;
int ret;
//如果创建线程的返回值不等于0,那么就是出错了
if ((ret=pthread_create(&tid, NULL, rout, NULL)) != 0 ) {
//从表准错误中输出错误信息
fprintf(stderr, "pthread_create : %s\n", strerror(ret));
return -1;
}
//在函数中进行循环打印内容
while(1) {
printf("i am main!\n");
sleep(1);
}
return 0;
}
线程ID
- 当我们在一个进程中创建好线程后,我们使用
ps -ef | grep 进程名字
查看后可以发现,只能看到一个进程信息,看不到所创建的线程信息,因此我们需要使用以下的这些命令来查看线程信息:
-ps -ef -L
:查看线程详细信息;
-ps -ef -L | grep 进程名字
:查看线程详细信息;
-ps aux -L
:查看线程详细信息,aux
选项前不需要加 -;
-ps aux -L | grep 进程名字
:查看线程详细信息,aux
选项前不需要加 -;
- 一个进程有多个线程,每个线程的 pcb 中都有一个 pid 和一个 tgid,
- pid:是每一个线程的 pcb 的 pid,也称为 LWP,如上图所示,这个值并不是我们操纵线程时所使用的 ID,操作线程的 ID 在下面会讲到;
- tgid:该值总是等于主线程的 pid,也是我们通过
ps -ef
所能查看的进程所显示的 pid,就是在前面进程操作博客中所讲到的进程 pcb 的pid;
pthread_t pthread_self()
:返回线程的 tid,这个是线程真正的 ID,通过这个 tid 就可以操作线程,这个值是一个地址——线程在进程中独有空间的首地址,这个返回值不能使用%d
来打印,而要使用%p
来打印,具体如上图中所示及下方代码演示;
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>
void *thr_entry(void *arg){
while(1) {
printf(" i am normal thread-%p-%d\n", pthread_self(), getpid());
sleep(1);
}
return NULL;
}
int main (int argc, char *argv[]){
pthread_t tid;
int ret;
//创建线程,去执行其他流程
ret = pthread_create(&tid, NULL, thr_entry, NULL);
if (ret != 0) {
printf("pthread_create failed!\n");
return -1;
}
while(1) {
printf("i am main thread--%p-%d\n", pthread_self(), getpid());
sleep(1);
}
return 0;
}
退出线程
- 从线程所执行的函数中 return,这种方法对主线程不适用,因为主线程就是 main 函数,而从 main 函数中 return 相当于调用
exit
,从而就结束进程了; void pthread_exit(void* value_ptr);
:在线程调用的函数中使用,可以终止当前线程;value_ptr
:指向一个区局的内存单元,不过我们在使用时一般置为空;- 返回值:无返回值,跟进程一样,线程结束的时候无法返回到它的调用者;
int pthread_ cancel(pthread_t tid);
终止同一进程中所指定的线程;tid
:创建线程时所获取到的线程ID;- 返回值:成功返回 0,失败返回错误码;
注意:
pthread_exit()
函数的参数或者 return 返回的指针所指向的内存单元必须是全局的或者是用malloc
分配的,不能是线程函数的栈上分配的,因为当其它线程得到这个返回指针时,线程函数已经退出了;- 主线程退出并不会导致进程退出,所有线程退出才会退出进程,而进程一旦只要退出,那么所有线程都会被退出;
线程等待
- 为什么:线程退出后,默认不会自动释放资源,仍然存在于进程的地址空间内,需要被等待;
- 作用:等待一个指定的线程退出,获取退出线程的返回值,回收线程资源,
int pthread_join(pthread_t tid, void** retval);
:阻塞等待指定线程退出,并获取线程的退出返回值;tid
:指定要等待的线程 tid;retval
:获取线程的 return 返回值,退出返回值为一级指针,所以我们要使用二级指针来获取;- 返回值:成功返回 0,失败返回错误编码;
调用该函数的线程将挂起等待,直到 id 为 tid 的线程终止,tid 线程以不同的方法终止,通过 pthread_join 得到的终止状态是不同的,总结如下:
- 如果 tid 线程是通过 return 返回,那么 retval 所指向的单元里存放的是 tid 线程函数的返回值;
- 如果 tid 线程被别的线程调用
pthread_ cancel
异常终掉,retval 所指向的单元里存放的是常数 PTHREAD_ CANCELED; - 如果 tid 线程是自己调用
pthread_exit
终止的,retval 所指向的单元存放的是传给pthread_exit
的参数; - 如果对 tid 线程的终止状态不感兴趣,可以传 NULL 给 retval 参数;
线程有个属性——分离属性,这个属性默认是joinjable状态,处于joinable状态的线程退出之后不会自动释放资源,需要被其他线程等待;
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>
//线程执行函数
void* thr_entry(void* arg){
char *ptr = "hello bit\n";
sleep(3);
return (void*)ptr;
}
int main (int argc, char *argv[]){
//创建线程执行其他流程
pthread_t tid;
int ret = pthread_create(&tid, NULL, thr_entry, NULL);
//创建失败则退出
if (ret != 0) {
printf("thread create error\n");
return -1;
}
//等待指定线程退出,并获取到其退出返回值
void *retval = NULL;
pthread_join(tid, &retval);
//打印出线程退出返回值
printf("retval:%s\n", retval);
while(1) {
printf("i am main thread\n");
sleep(1);
}
return 0;
}
线程分离
- 概念:默认情况下,新创建的线程是 joinable 属性的,线程退出后,需要对其进行
pthread_join
操作,否则无法释放资源,会造成系统泄漏;但是如果我并不关心线程的返回值,那么使用pthread_join
就是一种负担,这个时候,我们就需要告诉系统,当线程退出时,自动释放线程资源,我不需要获取任何东西; - 方法:将线程的分离属性设置为 detach 状态,处于 detach 状态的线程退出后,会自动释放资源,不需要被等待;
- 应用场景:不关心线程的退出返回值,也不想等待一个线程退出;
int pthread_detach(pthread_t tid);
:将指定线程设置为的 detach 状态;tid
:指定要改变状态的线程的 tid;- 返回值:成功返回 0,失败返回错误码(失败情况:线程本身就为 detach 状态或者没有该线程);
- 使用地方:可以在线程所执行的函数中将自身设置为 detach 状态,也可以在创建线程的流程中将其设置为 detach 状态;
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>
//线程所需执行的回调函数
void *thr_entry(void *arg){
//可以在回调函数中将自身线程设置为 detach 状态
pthread_detach(pthread_self());
char *ptr = "hello bit\n";
sleep(3);
return (void*)ptr;
}
int main (int argc, char *argv[]){
//创建线程执行其他流程
pthread_t tid;
int ret = pthread_create(&tid, NULL, thr_entry, NULL);
//创建失败则退出
if (ret != 0) {
printf("thread create error\n");
return -1;
}
//可以在线程被创建的线程中将其设置为 detach 状态
pthread_detach(tid);
while(1) {
printf("i am main thread\n");
sleep(1);
}
return 0;
}