当前位置: 首页 > news >正文

多线程基础 -概念、创建、等待、分离、终止

文章目录

  • 一、 线程概念
    • 1. 什么是线程
    • 2. 线程的优点
    • 3.线程的缺点
    • 4. 线程异常
    • 5. 线程用途
  • 二、 Linux进程VS线程
    • 1. 进程和线程
    • 2. 进程和线程的地址空间
    • 3. 进程和线程的关系
  • 三、Linux线程控制
    • 1. POSIX线程库
    • 2. 线程创建
    • 3. 线程ID及进程地址空间布局
    • 4. 线程终止
    • 5. 线程等待
    • 6. 线程分离


一、 线程概念

1. 什么是线程

在Linux中一个进程的创建意味着进程控制块PCB(task_struct),进程地址空间(mm_struct),和页表的建立。虚拟内存和物理内存之间的映射就是靠页表来完成的。
也就是说一个每一个进程都包含了独立的进程控制块PCB(task_struct),进程地址空间(mm_struct),和页表,这也是进程之间具有独立性的原因。

在这里插入图片描述
但我们多创建几个进程控制块(task_struct)但让他们共享统一个进程地址空间和页表如下图:
在这里插入图片描述
起始这本质上就是创建了4个线程

  • 我们说每一个线程是当先进程的一个执行流,也就是常说的线程是进程内部的一个执行分支
  • 同时每个线程都是在进程内部运行的,其本质上就是在进程地址空间内运行的,就是说,这个进程以前申请的所有资源都是被所有线程共享的。

值得注意的是进程不是有一个进程控制块task_struct就是一个进程,进程控制块,进程地址空间,页表,文件,信号等等,这些合起来叫一个进程。

而我们之前接触到的进程都只有一个task_struct,也就是该进程内部只有一个执行流就是只有一个进程控制块,即单执行流进程,反之,内部有多个执行流的进程叫做多执行流进程
在内核角度来看进程:进程是承担系统分配资源的实体
而线程是cpu调度的基本单位。

那么在cpu内部能区分自己调度的task_struct是线程还是进程吗?

答案是当然不行,也没必要,因为cpu只关心一个一个的执行流,无论是单执行流还是多执行流,cpu才不会管呢,他就负责执行,才不会管你是啥。

在这里插入图片描述

多执行流时cpu调度:
在这里插入图片描述
单执行流时线程调度:
在这里插入图片描述

在一个系统中存在大量的进程,而一个进程中又存在大量线程,那么系统中肯定存在着大量的线程,那这么多线程需不需要管理呢?当然是需要的,那这么管理呢?当然是六字真言:先描述再组织。先把描述线程的变量描述在一个结构体当中,然后再利用某种数据结构比如链表将一个个的结构体组织起来。这么一来对线程的增加删除,就变成了对链表的增删查改。

但在Linux中是没有真正意义上的线程的,因为Linux没有专门设计线程的管理,因为线程和进程结构上比较类似,所以对进程的管理方法进行了复用,因此我们称Linux中的线程为轻量化的进程。

而在Windows中是存在真正的线程的,因此Windows当中对于线程管理的设计一定比Linux当中的更复杂。

既然在Linux没有真正意义的线程,那么也就绝对没有真正意义上的线程相关的系统调用!

既然在Linux中都没有真正意义上的线程了,那么自然也没有真正意义上的线程相关的系统调用了。但是Linux可以提供创建轻量级进程的接口,也就是创建进程,共享空间,其中最典型的代表就是vfork函数。

vfork函数的功能就是创建子进程,但是父子共享空间,v函数fork的函数原型如下:

pid_t vfork(void);

vfork函数的返回值与fork函数的返回值相同:

  • 给父进程返回子进程的PID。
  • 给子进程返回0。

只不过vfork函数创建出来的子进程与其父进程共享地址空间,符合线程的定义。
例如在下面的代码中,父进程使用vfork函数创建子进程,子进程将全局变量g_val由100改为了200,父进程休眠3秒后再读取到全局变量g_val的值。

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
int g_val = 100;
int main()
{pid_t id = vfork();if (id == 0){//childg_val = 200;printf("child:PID:%d, PPID:%d, g_val:%d\n", getpid(), getppid(), g_val);exit(0);}//fathersleep(3);printf("father:PID:%d, PPID:%d, g_val:%d\n", getpid(), getppid(), g_val);return 0;
}

在这里插入图片描述
可以看到,父进程读取到g_val的值是子进程修改后的值,也就证明了vfork创建的子进程与其父进程是共享地址空间的。

但在我们想要创建一个线程的时候更常用的是pthread_create这样的原生线程库封装的函数而不是vfork。
原生线程库实际就是对轻量级进程的系统调用进行了封装,在用户层模拟实现了一套线程相关的接口。
因此对于我们来讲,在Linux下学习线程实际上就是学习在用户层模拟实现的这一套接口,而并非操作系统的接口。

总结

  • 在一个程序里的一个执行路线就叫做线程(thread)。更准确的定义是:线程是“一个进程内部的控制序列”
  • 一切进程至少都有一个执行线程
  • 线程在进程内部运行,本质是在进程地址空间内运行
  • 在Linux系统中,在CPU眼中,看到的PCB都要比传统的进程更加轻量化
  • 透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流

在这里插入图片描述

2. 线程的优点

  • 创建一个新线程的代价要比创建一个新进程小得多
  • 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
  • 线程占用的资源要比进程少很多
  • 能充分利用多处理器的可并行数量
  • 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
  • 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
  • I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。

3.线程的缺点

  • 性能损失
    • 一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。
  • 健壮性降低
    • 编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。
  • 缺乏访问控制
    • 进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。
  • 编程难度提高
    • 编写与调试一个多线程程序比单线程程序困难得多

4. 线程异常

  • 单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃
  • 线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出

5. 线程用途

  • 合理的使用多线程,能提高CPU密集型程序的执行效率
  • 合理的使用多线程,能提高IO密集型程序的用户体验(如生活中我们一边写代码一边下载开发工具,就是多线程运行的一种表现)

二、 Linux进程VS线程

1. 进程和线程

  • 进程是资源分配的基本单位
  • 线程是调度的基本单位
  • 线程共享进程数据,但也拥有自己的一部分数据:
    • 线程ID
    • 一组寄存器
    • errno
    • 信号屏蔽字
    • 调度优先级

2. 进程和线程的地址空间

进程的多个线程共享 同一地址空间,因此Text Segment、Data Segment都是共享的,如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境:

  • 文件描述符表
  • 每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)
  • 当前工作目录
  • 用户id和组id

3. 进程和线程的关系

进程和线程的关系如下图:
在这里插入图片描述

三、Linux线程控制

1. POSIX线程库

pthread线程库是应用层的原生线程库:

  • 应用层指的是这个线程库并不是系统接口直接提供的,而是由第三方帮我们提供的。
  • 原生指的是大部分Linux系统都会默认带上该线程库。
  • 与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以“pthread_”打头的。
  • 要使用这些函数库,要通过引入头文件<pthreaad.h>。
  • 链接这些线程函数库时,要使用编译器命令的“-lpthread”选项。

错误检查:

  • 传统的一些函数是,成功返回0,失败返回-1,并且对全局变量errno赋值以指示错误。
  • pthreads函数出错时不会设置全局变量errno(而大部分POSIX函数会这样做),而是将错误代码通过返回值返回。
  • pthreads同样也提供了线程内的errno变量,以支持其他使用errno的代码。对于pthreads函数的错误,建议通过返回值来判定,因为读取返回值要比读取线程内的errno变量的开销更小。

2. 线程创建

线程创建需要调用pthread_create函数,这是函数原型

int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);

参数说明:

  • thread:获取创建成功的线程ID,该参数是一个输出型参数。
  • attr:用于设置创建线程的属性,一般传入NULL表示使用默认属性。
  • start_routine:该参数是一个函数地址,表示线程例程,即线程启动后要执行的函数。
  • arg:传给线程例程的参数。

如果创建线程成功则返回0,失败返回错误码
当一个程序启动时,就有一个进程被创建,同时也有一个线程开始运行,我们把这个线程叫做主线程
主线程的作用:

  • 在主线程中创建其他的线程
  • 在主线程中完成各种善后工作,比如线程等待等等。

从函数原型中可以看出第三个参数是一个函数指针,并且这个函数只有一个参数那就是void* 返回值也必须是void* 。当线程创建好之后,这个线程就会执行该函数中的代码,即新的执行流。

最后一个参数是传给线程所执行函数的参数,类型也必须为void*类型,所以传参的时候要注意强制类型转换。

下面我们让主线程调用pthread_create函数创建一个新线程,此后新线程就会跑去执行自己的新例程,而主线程则继续执行后续代码。

#include <stdio.h>
#include <pthread.h>  //需要包含的头文件
#include <unistd.h>void* Routine(void* arg)
{char* msg = (char*)arg;while (1){printf("I am %s\n", msg);sleep(1);}
}
int main()
{pthread_t tid;//第一个参数,输出型参数,tid就是线程idpthread_create(&tid, NULL, Routine, (void*)"thread 1");while (1){printf("I am main thread!\n");sleep(2);}return 0;
}

可以看到每隔两秒主线程输出一次,每隔一秒子线程输出一次
在这里插入图片描述
利用ps -axj命令查看进程,我们发现只有一个进程,和预想中的符合,因为本来就是一个进程,然后进程内有两个线程
在这里插入图片描述
我们可以使用ps -aL命令查看一下当前的线程就几个

  • 默认情况下,不带-L,看到的就是一个个的进程。
  • 带-L就可以查看到每个进程内的多个轻量级进程。

在这里插入图片描述
可以看到有两个ceshi,他们的PID一样也就是进程ID一样但是LWP不一样,那LWP是啥呢?LWP起始就是Lightweight process,轻量化进程的意思,也就是线程ID,可以看到两个ceshi的线程ID是不一样的,说明这是两个线程。其中一个线程的LWP和PID一样,说明他是主线程。
我们之前接触到的都是单线程进程,其PID和LWP是相等的,所以对于单线程进程来说,调度时采用PID和LWP是一样的。

为了进一步说明这两线程属于同一个进程,我们可以让两个线程把他们的PID和PPID都打印出来。

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>void* Routine(void* arg)
{char* msg = (char*)arg;while (1){printf("I am %s...pid: %d, ppid: %d\n", msg, getpid(), getppid());sleep(1);}
}
int main()
{pthread_t tid;pthread_create(&tid, NULL, Routine, (void*)"thread 1");while (1){printf("I am main thread...pid: %d, ppid: %d\n", getpid(), getppid());sleep(2);}return 0;
}

在这里插入图片描述
可以看到主线程和新线程的PID和PPID是一样的,也就是说主线程和新线程虽然是两个执行流,但它们仍然属于同一个进程。

当然我们也可以利用循环直接创建一批线程,然后让新线程都去执行同一个函数,此时这个函数会被重复执行,我们称这个函数是重入的。

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>void* Routine(void* arg)
{char* msg = (char*)arg;while (1){printf("I am %s...pid: %d, ppid: %d\n", msg, getpid(), getppid());sleep(1);}
}
int main()
{pthread_t tid[5];for (int i = 0; i < 5; i++){char* buffer = (char*)malloc(64);sprintf(buffer, "thread %d", i);pthread_create(&tid[i], NULL, Routine, buffer);}while (1){printf("I am main thread...pid: %d, ppid: %d\n", getpid(), getppid());sleep(2);}return 0;
}

在这里插入图片描述
可以看出,同时运行了六个线程并且这六个线程PID一样,属于同一个进程。
在这里插入图片描述

3. 线程ID及进程地址空间布局

线程id有两种获取方式

  • 第一种是在线程建立时通过输出型参数pthread_t *thread来获得
  • 第二种是调用pthread_self 函数进行获取,哪个线程调用这个函数,这个函数就会返回哪个线程的线程id

pthread_self的函数原型

pthread_t pthread_self(void);

下面的代码展示了主线程中创建了五个子线程,每次创建一个线程之后通过输出型参数输出所创建线程的ID,然后在子线程中每个子线程通过调用==pthread_self()函数输出自己的线程ID,最后在主线程中调用pthread_self()==输出自己的线程ID

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>void* Routine(void* arg)
{char* msg = (char*)arg;while (1){printf("I am %s...pid: %d, ppid: %d, tid: %lu\n", msg, getpid(), getppid(), pthread_self());//函数中通过调用pthread_self()输出子线程ID//哪个子线程调用它就输出哪个子线程的IDsleep(1);}
}
int main()
{pthread_t tid[5];for (int i = 0; i < 5; i++){char* buffer = (char*)malloc(64);sprintf(buffer, "thread %d", i);pthread_create(&tid[i], NULL, Routine, buffer);printf("%s tid is %lu\n", buffer, tid[i]);//主函数中通过输出型参数输出以此线程ID}while (1){printf("I am main thread...pid: %d, ppid: %d, tid: %lu\n", getpid(), getppid(), pthread_self());//主线程调用pthread_self()函数输出主线程IDsleep(2);}return 0;
}

在这里插入图片描述
可以看到两种方式获取的线程ID其实是一样的(当然一样,不一样就怪了)。另外用pthread_self函数获得的线程ID与内核的LWP的值是不相等的,pthread_self函数获得的是用户级原生线程库的线程ID,而LWP是内核的轻量级进程ID,它们之间是一对一的关系。

关于线程ID和进程地址空间的那些事

  • pthread_create函数会产生一个线程ID,存放在第一个参数指向的地址中,该线程ID和内核中的LWP不是一回事。
  • 内核中的LWP属于进程调度的范畴,因为线程是轻量级进程,是操作系统调度器的最小单位,所以需要一个数值来唯一表示该线程。
  • pthread_create函数第一个参数指向一个虚拟内存单元,该内存单元的地址即为新创建线程的线程ID,这个ID属于NPTL线程库的范畴,线程库的后续操作就是根据该线程ID来操作线程的。
  • 线程库NPTL提供的pthread_self函数,获取的线程ID和pthread_create函数第一个参数获取的线程ID是一样的。

Linux不提供真正的线程,只提供LWP,也就意味着操作系统只需要对内核执行流LWP进行管理,而供用户使用的线程接口等其他数据,应该由线程库自己来管理因此管理线程时的“先描述,再组织”就应该在线程库里进行。

既然是在线程库里进行先描述在组织那线程库在哪里呢?
同通过ldd指令我们可以看出线程库是一个动态库,因此在进程建立的时候,线程库会被加载到进程地址空间中的共享区。又因为线程之间进程地址空间是共享的,所以所有线程都可以看到这个库。
在这里插入图片描述
在这里插入图片描述
每个线程都有自己私有的栈,主线程采用的栈是进程地址空间中的栈,而其余线程采用的栈就是在共享区中开辟的。每个线程都有自己的struct pthread,当中包含了对应线程的各种属性;每个线程还有自己的线程局部存储,当中包含了对应线程被切换时的上下文数据。
这些东西都在共享区中存储,因此我们只要知道数据在共享区中的地址,就可以靠地址找到它们,进行管理。因此我们说其实线程ID就是进程地址空间中的一个地址罢了。
上面我们所用的各种线程函数,本质都是在库内部对线程属性进行的各种操作,最后将要执行的代码交给对应的内核级LWP去执行就行了,也就是说线程数据的管理本质是在共享区的。

pthread_t到底是什么类型取决于实现,但是对于Linux目前实现的NPTL线程库来说,线程ID本质就是进程地址空间共享区上的一个虚拟地址,同一个进程中所有的虚拟地址都是不同的,因此可以用它来唯一区分每一个线程。

例如,我们也可以尝试按地址的形式对获取到的线程ID进行打印。

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>void* Routine(void* arg)
{while (1){printf("new  thread tid: %p\n", pthread_self());sleep(1);}
}
int main()
{pthread_t tid;pthread_create(&tid, NULL, Routine, NULL);while (1){printf("main thread tid: %p\n", pthread_self());sleep(2);}return 0;
}

在这里插入图片描述

可以看出线程ID本质上就是地址。

4. 线程终止

如果需要只终止某个线程而不终止整个进程,可以有三种方法:

  1. 从线程函数return。这种方法对主线程不适用,从main函数return相当于调用exit。
  2. 线程可以调用pthread_ exit终止自己。
  3. 一个线程可以调用pthread_ cancel终止同一进程中的另一个线程。

return退出:

#include <iostream>
#include <pthread.h>
#include <sys/types.h>
#include <unistd.h>void *Routine(void *args)
{char *msg = (char *)args;int cnt = 0;while (cnt < 5){printf("I am %s...pid: %d, ppid: %d, tid: %lu\n", msg, getpid(), getppid(), pthread_self());sleep(1);cnt++;}return (void *)2024;
}int main()
{pthread_t tid[5];for (int i = 0; i < 5; i++){char *buffer = (char *)malloc(64);sprintf(buffer, "pthread %d", i);pthread_create(&tid[i], nullptr, Routine, buffer);printf("%s tid is %lu\n", buffer, tid[i]);}return 0;
}

可以看到并没有执行子线程中的输出语句,那是因为我们这里没有阻塞的进行线程等待而是直接执行完return语句,主线程直接退出了,主线程退出了,进程直接就结束了,自然其他线程也不会执行输出语句。
在这里插入图片描述

通过pthread_exit()函数进行终止

pthread_exit函数
功能:线程终止
原型:

void pthread_exit(void *value_ptr);

参数:

  • value_ptr:value_ptr不要指向一个局部变量。
  • 返回值:无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)
  • 需要注意,pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。

此处我们创建五个线程,输出5次之后利用pthread_exit函数返回8888

#include <iostream>
#include <pthread.h>
#include <sys/types.h>
#include <unistd.h>void *Routine(void *args)
{char *msg = (char *)args;int cnt = 0;while (cnt < 5){printf("I am %s...pid: %d, ppid: %d, tid: %lu\n", msg, getpid(), getppid(), pthread_self());sleep(1);cnt++;}pthread_exit((void *)8888);
}int main()
{pthread_t tid[5];for (int i = 0; i < 5; i++){char *buffer = (char *)malloc(64);sprintf(buffer, "pthread %d", i);pthread_create(&tid[i], nullptr, Routine, buffer);printf("%s tid is %lu\n", buffer, tid[i]);}printf("I am main thread...pid: %d, ppid: %d, tid: %lu\n", getpid(), getppid(), pthread_self());for (int i = 0; i < 5; i++){void *ret = nullptr;pthread_join(tid[i], &ret);printf("thread %d[%lu]...quit, exitcode: %lld\n", i, tid[i], (long long int)ret);}return 0;
}

在这里插入图片描述
注意exit函数的作用是终止进程,任何一个线程调用exit函数也代表的是整个进程终止。

pthread_cancel(pthread_self())终止线程

pthread_cancel函数
功能: 取消一个执行中的线程
原型:

int pthread_cancel(pthread_t thread);

参数:

  • thread:线程ID
  • 返回值:成功返回0;失败返回错误码

此处我们创建5个线程,在线程中调用pthread_cancel函数终止自己,在主线程中输出退出码,按我们上面说的,退出码应该是-1。

#include <iostream>
#include <pthread.h>
#include <sys/types.h>
#include <unistd.h>void *Routine(void *args)
{char *msg = (char *)args;int cnt = 0;while (cnt < 5){printf("I am %s...pid: %d, ppid: %d, tid: %lu\n", msg, getpid(), getppid(), pthread_self());sleep(1);cnt++;pthread_cancel(pthread_self());}return (void *)2024;// pthread_exit((void *)6666);
}int main()
{pthread_t tid[5];for (int i = 0; i < 5; i++){char *buffer = (char *)malloc(64);sprintf(buffer, "pthread %d", i);pthread_create(&tid[i], nullptr, Routine, buffer);printf("%s tid is %lu\n", buffer, tid[i]);}printf("I am main thread...pid: %d, ppid: %d, tid: %lu\n", getpid(), getppid(), pthread_self());for (int i = 0; i < 5; i++){void *ret = nullptr;pthread_join(tid[i], &ret);printf("thread %d[%lu]...quit, exitcode: %lld\n", i, tid[i], (long long int)ret);}return 0;
}

在这里插入图片描述

这个函数不仅可以自己取消自己,也可以在其他线程中取消其他线程。比如我们在主线程中通过调用pthread_cancle()取消同一进程中的其他4个线程。

#include <iostream>
#include <pthread.h>
#include <sys/types.h>
#include <unistd.h>void *Routine(void *args)
{char *msg = (char *)args;int cnt = 0;while (cnt < 5){printf("I am %s...pid: %d, ppid: %d, tid: %lu\n", msg, getpid(), getppid(), pthread_self());sleep(1);cnt++;}pthread_exit((void *)8888);
}int main()
{pthread_t tid[5];for (int i = 0; i < 5; i++){char *buffer = (char *)malloc(64);sprintf(buffer, "pthread %d", i);pthread_create(&tid[i], nullptr, Routine, buffer);printf("%s tid is %lu\n", buffer, tid[i]);}printf("I am main thread...pid: %d, ppid: %d, tid: %lu\n", getpid(), getppid(), pthread_self());pthread_cancel(tid[0]);pthread_cancel(tid[1]);pthread_cancel(tid[2]);pthread_cancel(tid[3]);for (int i = 0; i < 5; i++){void *ret = nullptr;pthread_join(tid[i], &ret);printf("thread %d[%lu]...quit, exitcode: %lld\n", i, tid[i], (long long int)ret);}return 0;
}

可以看到四个线程直接退出,退出码是1,最后剩余一个线程由于没有被终止,因此输出五次之后正常退出,退出码为8888.
在这里插入图片描述

5. 线程等待

为什么要线程等待?

  • 已经退出的线程,其空间没有被释放,仍然在进程的地址空间内。
  • 创建新的线程不会复用刚才退出线程的地址空间。

功能:等待线程结束
原型

int pthread_join(pthread_t thread, void **value_ptr);

参数

  • thread:线程ID
  • value_ptr:它指向一个指针,后者指向线程的返回值
  • 返回值:成功返回0;失败返回错误码

调用该函数的线程将挂起等待,直到id为thread的线程终止。thread线程以不同的方法终止,通过pthread_join得到的终止状态是不同的,总结如下:

  1. 如果thread线程通过return返回,value_ ptr所指向的单元里存放的是thread线程函数的返回值。
  2. 如果thread线程被别的线程调用pthread_ cancel异常终掉,value_ ptr所指向的单元里存放的是常数
    PTHREAD_ CANCELED。用grep命令进行查找,可以发现PTHREAD_CANCELED实际上就是头文件<pthread.h>里面的一个宏定义,它的值本质就是-1。
  3. 如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传给pthread_exit的参
    数。
  4. 如果对thread线程的终止状态不感兴趣,可以传NULL给value_ ptr参数

在这里插入图片描述

#include <iostream>
#include <pthread.h>
#include <sys/types.h>
#include <unistd.h>void *Routine(void *args)
{char *msg = (char *)args;int cnt = 0;while (cnt < 5){printf("I am %s...pid: %d, ppid: %d, tid: %lu\n", msg, getpid(), getppid(), pthread_self());sleep(1);cnt++;}pthread_exit((void *)0);
}int main()
{pthread_t tid[5];for (int i = 0; i < 5; i++){char *buffer = (char *)malloc(64);sprintf(buffer, "pthread %d", i);pthread_create(&tid[i], nullptr, Routine, buffer);printf("%s tid is %lu\n", buffer, tid[i]);}printf("I am main thread...pid: %d, ppid: %d, tid: %lu\n", getpid(), getppid(), pthread_self());for (int i = 0; i < 5; i++){pthread_join(tid[i], nullptr);printf("thread %d[%lu]...quit\n", i, tid[i]);}return 0;
}

可以看出主线程成功对这五个线程进行了等待。
在这里插入图片描述
面我们再来看看如何获取线程退出时的退出码,为了便于查看,我们这里将线程退出时的退出码设置为某个特殊的值,比如2024,并在成功等待线程后将该线程的退出码进行输出。
注意输出时要强转成long long int

#include <iostream>
#include <pthread.h>
#include <sys/types.h>
#include <unistd.h>void *Routine(void *args)
{char *msg = (char *)args;int cnt = 0;while (cnt < 5){printf("I am %s...pid: %d, ppid: %d, tid: %lu\n", msg, getpid(), getppid(), pthread_self());sleep(1);cnt++;}return (void *)2024;
}int main()
{pthread_t tid[5];for (int i = 0; i < 5; i++){char *buffer = (char *)malloc(64);sprintf(buffer, "pthread %d", i);pthread_create(&tid[i], nullptr, Routine, buffer);printf("%s tid is %lu\n", buffer, tid[i]);}printf("I am main thread...pid: %d, ppid: %d, tid: %lu\n", getpid(), getppid(), pthread_self());for (int i = 0; i < 5; i++){void *ret = nullptr;pthread_join(tid[i], &ret);printf("thread %d[%lu]...quit, exitcode: %lld\n", i, tid[i], (long long int)ret);}return 0;
}

可以看到线程等待成功,并且返回值为2024.
在这里插入图片描述
注意: pthread_join函数默认是以阻塞的方式进行线程等待的。

为什么线程退出时只能拿到线程的退出码?

如果我们等待的是一个进程,那么当这个进程退出时,我们可以通过wait函数或是waitpid函数的输出型参数status,获取到退出进程的退出码、退出信号以及core dump标志。

那为什么等待线程时我们只能拿到退出线程的退出码?难道线程不会出现异常吗?

线程在运行过程中当然也会出现异常,线程和进程一样,线程退出的情况也有三种:

  1. 代码运行完毕,结果正确。
  2. 代码运行完毕,结果不正确。
  3. 代码异常终止。
    因此我们也需要考虑线程异常终止的情况,但是pthread_join函数无法获取到线程异常退出时的信息。因为线程是进程内的一个执行分支,如果进程中的某个线程崩溃了,那么整个进程也会因此而崩溃,此时我们根本没办法执行pthread_join函数,因为整个进程已经退出了。

例如,我们在线程的执行例程当中制造一个除零错误,当某一个线程执行到此处时就会崩溃,进而导致整个进程崩溃。

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>void* Routine(void* arg)
{char* msg = (char*)arg;int count = 0;while (count < 5){printf("I am %s...pid: %d, ppid: %d, tid: %lu\n", msg, getpid(), getppid(), pthread_self());sleep(1);count++;int a = 1 / 0; //error}return (void*)2022;
}
int main()
{pthread_t tid[5];for (int i = 0; i < 5; i++){char* buffer = (char*)malloc(64);sprintf(buffer, "thread %d", i);pthread_create(&tid[i], NULL, Routine, buffer);printf("%s tid is %lu\n", buffer, tid[i]);}printf("I am main thread...pid: %d, ppid: %d, tid: %lu\n", getpid(), getppid(), pthread_self());for (int i = 0; i < 5; i++){void* ret = NULL;pthread_join(tid[i], &ret);printf("thread %d[%lu]...quit, exitcode: %d\n", i, tid[i], (int)ret);}return 0;
}
**

一个线程挂了,全部线程就都挂了,所以我们也不知道到底是哪个线程出了问题,可见多线程健壮性不强。
在这里插入图片描述

所以pthread_join函数只能获取到线程正常退出时的退出码,用于判断线程的运行结果是否正确。

6. 线程分离

  • 默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。
  • 如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。
  • 一个线程如果被分离了,这个线程依旧要使用该进程的资源,依旧在该进程内运行,甚至这个线程崩溃了一定会影响其他线程,只不过这个线程退出时不再需要主线程去join了,当这个线程退出时系统会自动回收该线程所对应的资源。
  • 可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离:
  • joinable和分离是冲突的,一个线程不能既是joinable又是分离的。
int pthread_detach(pthread_t thread);
pthread_detach(pthread_self());

参数说明:

  • thread:被分离线程的ID。

返回值说明:

  • 线程分离成功返回0,失败返回错误码。

我们可以在线程中调用这个函数,这样主线程中就不需要进行线程等待,子线程运行结束之后,系统会自动回收资源。

值得注意的是虽然主线程不需要等待了,但还是需要让主线程最后退出,如果主线程提前退出了,相当于进程直接结束了,那其他线程也就直接结束了。

#include <iostream>
#include <pthread.h>
#include <sys/types.h>
#include <unistd.h>void *Routine(void *args)
{pthread_detach(pthread_self());//线程调用该函数分离自己char *msg = (char *)args;int cnt = 0;while (cnt < 5){printf("I am %s...pid: %d, ppid: %d, tid: %lu\n", msg, getpid(), getppid(), pthread_self());sleep(1);cnt++;}// return (void *)2024;pthread_exit((void *)6666);
}int main()
{pthread_t tid[5];for (int i = 0; i < 5; i++){char *buffer = (char *)malloc(64);sprintf(buffer, "pthread %d", i);pthread_create(&tid[i], nullptr, Routine, buffer);printf("%s tid is %lu\n", buffer, tid[i]);}while (1){printf("I am main thread...pid: %d, ppid: %d, tid: %lu\n", getpid(), getppid(), pthread_self());sleep(1);}return 0;
}

相关文章:

多线程基础 -概念、创建、等待、分离、终止

文章目录 一、 线程概念1. 什么是线程2. 线程的优点3.线程的缺点4. 线程异常5. 线程用途 二、 Linux进程VS线程1. 进程和线程2. 进程和线程的地址空间3. 进程和线程的关系 三、Linux线程控制1. POSIX线程库2. 线程创建3. 线程ID及进程地址空间布局4. 线程终止5. 线程等待6. 线程…...

【Vue3】走进Pinia,学习Pinia,使用Pinia

&#x1f497;&#x1f497;&#x1f497;欢迎来到我的博客&#xff0c;你将找到有关如何使用技术解决问题的文章&#xff0c;也会找到某个技术的学习路线。无论你是何种职业&#xff0c;我都希望我的博客对你有所帮助。最后不要忘记订阅我的博客以获取最新文章&#xff0c;也欢…...

【TB作品】430单片机,单片机串口多功能通信,Proteus仿真

文章目录 题目功能仿真图程序介绍代码、仿真、原理图、PCB 题目 60、单片机串口多功能通信 基本要求: 设计一串口通信程序,波特率38400,通过RS232与PC机通信。 自动循环发送数据串(设计在程序中) 接收并存储和显示该数据串 在发送端定义10个ASCII码键0-9 按键发送单字节,PC机接…...

【C++ leetcode】双指针问题

1. 611. 有效三角形的个数 题目 给定一个包含非负整数的数组 nums &#xff0c;返回其中可以组成三角形三条边的三元组个数。 题目链接 . - 力扣&#xff08;LeetCode&#xff09; 画图 和 文字 分析 判断是否是三角形要得到三边&#xff0c;由于遍历三边要套三层循环&#x…...

Kubernetes集群部署

1.集群环境搭建 1.1 环境规划 kubernetes集群大体上分为两类&#xff1a;一主多从和多主多从。 一主多从&#xff1a;一台Master节点和多台Node节点&#xff0c;搭建简单&#xff0c;但是有单机故障风险&#xff0c;适合用于测试环境多主多从&#xff1a;多台Master节点和多…...

深拷贝与浅拷贝

深拷贝与浅拷贝是在进行对象复制时常见的两种方式&#xff0c;这两个概念其实比较混淆&#xff0c;面试中也经常出现&#xff0c;但是实际开发很少用到&#xff0c;所以本文就来详细讲解一下&#xff0c;让大家不再迷惑。 浅拷贝只是复制了对象的引用&#xff08;地址&#xf…...

golang学习网址

.1LearnKu 终身编程者的知识社区 https://learnku.com/...

2024学习鸿蒙开发,未来发展如何?

一、前言 想要了解一个领域的未来发展如何&#xff0c;可以从如下几点进行&#xff0c;避免盲从&#xff1a; 国家政策落地情况就业市场如何学习 通过上述三点&#xff0c;就能分析出一个行业的趋势。大家可以看到&#xff0c;我上面的总体逻辑就是根据国家政策来分析未来方…...

3.21Code

基于二叉链表的二叉树最大宽度的计算 #include<iostream>#define MAXSIZE 1000using namespace std;int k0; int m0; //记录层数 typedef struct BiNode{char data;struct BiNode *lchild;struct BiNode *rchild; }BiNode,*BiTree;void CreateBiTree(BiTree &T){cha…...

学习总结2

解题思路 用bfs进行搜索,标记A罐B罐所保存的水的出现情况,当再次出现的时候停止搜索,然后用数组模拟链表进行保存路径.最后输出. 代码 #include <iostream> #include <cstdio> #include <fstream> #include <algorithm> #include <cmath> #in…...

【LeetCode】--- 动态规划 集训(一)

目录 一、1137. 第 N 个泰波那契数1.1 题目解析1.2 状态转移方程1.3 解题代码 二、面试题 08.01. 三步问题2.1 题目解析2.2 状态转移方程2.3 解题代码 三、746. 使用最小花费爬楼梯3.1 题目解析3.2 状态转移方程3.3 解题代码 一、1137. 第 N 个泰波那契数 题目地址&#xff1a…...

【数据结构与算法】(18):树形选择排序:按照锦标赛的思想进行排序

&#x1f921;博客主页&#xff1a;Code_文晓 &#x1f970;本文专栏&#xff1a;数据结构与算法 &#x1f63b;欢迎关注&#xff1a;感谢大家的点赞评论关注&#xff0c;祝您学有所成&#xff01; ✨✨&#x1f49c;&#x1f49b;想要学习更多数据结构与算法点击专栏链接查看&…...

统计单词数

统计单词数 题目描述 一般的文本编辑器都有查找单词的功能&#xff0c;该功能可以快速定位特定单词在文章中的位置&#xff0c;有的还能统计出特定单词在文章中出现的次数。 现在&#xff0c;请你编程实现这一功能&#xff0c;具体要求是&#xff1a;给定一个单词&#xff0…...

c++pair的用法

pair简单来说就是可以存储两种类型数据的一个类&#xff0c;其内部是使用模板实现的&#xff0c;所以可以指定其内部的类型。 pair在#include <utility> pair的构造 pair<int, string> p1({ 1,"张三" });pair<int, string> p2;pair<int, str…...

石油炼化5G智能制造工厂数字孪生可视化平台,推进行业数字化转型

石油炼化5G智能制造工厂数字孪生可视化平台&#xff0c;推进行业数字化转型。在石油炼化行业&#xff0c;5G智能制造工厂数字孪生可视化平台的出现&#xff0c;为行业的数字化转型注入了新的活力。石油炼化行业作为传统工业的重要领域&#xff0c;面临着资源紧张、环境压力、安…...

IP代理技术革新:探索数据采集的新路径

引言&#xff1a; 随着全球化进程不断加深&#xff0c;网络数据采集在企业决策和市场分析中扮演着愈发重要的角色。然而&#xff0c;地域限制和IP封锁等问题常常给数据采集工作带来了巨大挑战。亿牛云代理服务凭借其强大的网络覆盖和真实住宅IP资源&#xff0c;成为解决这些问…...

流畅的 Python 第二版(GPT 重译)(一)

前言 计划是这样的&#xff1a;当有人使用你不理解的特性时&#xff0c;直接开枪打死他们。这比学习新东西要容易得多&#xff0c;不久之后&#xff0c;活下来的程序员只会用一个容易理解的、微小的 Python 0.9.6 子集来编写代码 。 Tim Peters&#xff0c;传奇的核心开发者&am…...

Vue+jquery+jquery.maphilight实现图片热区高亮以及点击效果

//鼠标悬浮效果 mounted() {this.setCurrentTask(0); //对于id为mapAll的热区图&#xff0c;设置鼠标放置在上面有一个颜色 fillColor填充颜色 strokeColor边框颜色 strokeWidth边框宽度 fillOpacity 是设置热区填充颜色的不透明度的属性。 alwaysOn:true 保持常量$(function(…...

靠谱!朋友圈一键转发和自动转发好友朋友圈

微信朋友圈在生活和工作中扮演着重要的社交和信息传播角色。尤其是对于一些企业来说&#xff0c;朋友圈是不可或缺的推广渠道。 今天就给大家分享一个能够实现一键转发和自动转发好友朋友圈的工具——微信管理系统&#xff0c;让大家都能有效的管理朋友圈。 1、定时发圈&…...

线性顺序表算法库

list.cpp 具体函数实现 #include <stdio.h> #include "list.h" #include <malloc.h>/************************************************** ①函数名: CreateList 功 能: 用数组构建顺序表 参 数: ①SqList *&L:传入的线性表 ②ElemType a[]:使用…...

浅谈 React Hooks

React Hooks 是 React 16.8 引入的一组 API&#xff0c;用于在函数组件中使用 state 和其他 React 特性&#xff08;例如生命周期方法、context 等&#xff09;。Hooks 通过简洁的函数接口&#xff0c;解决了状态与 UI 的高度解耦&#xff0c;通过函数式编程范式实现更灵活 Rea…...

2025年能源电力系统与流体力学国际会议 (EPSFD 2025)

2025年能源电力系统与流体力学国际会议&#xff08;EPSFD 2025&#xff09;将于本年度在美丽的杭州盛大召开。作为全球能源、电力系统以及流体力学领域的顶级盛会&#xff0c;EPSFD 2025旨在为来自世界各地的科学家、工程师和研究人员提供一个展示最新研究成果、分享实践经验及…...

3.3.1_1 检错编码(奇偶校验码)

从这节课开始&#xff0c;我们会探讨数据链路层的差错控制功能&#xff0c;差错控制功能的主要目标是要发现并且解决一个帧内部的位错误&#xff0c;我们需要使用特殊的编码技术去发现帧内部的位错误&#xff0c;当我们发现位错误之后&#xff0c;通常来说有两种解决方案。第一…...

如何在看板中体现优先级变化

在看板中有效体现优先级变化的关键措施包括&#xff1a;采用颜色或标签标识优先级、设置任务排序规则、使用独立的优先级列或泳道、结合自动化规则同步优先级变化、建立定期的优先级审查流程。其中&#xff0c;设置任务排序规则尤其重要&#xff0c;因为它让看板视觉上直观地体…...

【机器视觉】单目测距——运动结构恢复

ps&#xff1a;图是随便找的&#xff0c;为了凑个封面 前言 在前面对光流法进行进一步改进&#xff0c;希望将2D光流推广至3D场景流时&#xff0c;发现2D转3D过程中存在尺度歧义问题&#xff0c;需要补全摄像头拍摄图像中缺失的深度信息&#xff0c;否则解空间不收敛&#xf…...

【RockeMQ】第2节|RocketMQ快速实战以及核⼼概念详解(二)

升级Dledger高可用集群 一、主从架构的不足与Dledger的定位 主从架构缺陷 数据备份依赖Slave节点&#xff0c;但无自动故障转移能力&#xff0c;Master宕机后需人工切换&#xff0c;期间消息可能无法读取。Slave仅存储数据&#xff0c;无法主动升级为Master响应请求&#xff…...

2023赣州旅游投资集团

单选题 1.“不登高山&#xff0c;不知天之高也&#xff1b;不临深溪&#xff0c;不知地之厚也。”这句话说明_____。 A、人的意识具有创造性 B、人的认识是独立于实践之外的 C、实践在认识过程中具有决定作用 D、人的一切知识都是从直接经验中获得的 参考答案: C 本题解…...

LLMs 系列实操科普(1)

写在前面&#xff1a; 本期内容我们继续 Andrej Karpathy 的《How I use LLMs》讲座内容&#xff0c;原视频时长 ~130 分钟&#xff0c;以实操演示主流的一些 LLMs 的使用&#xff0c;由于涉及到实操&#xff0c;实际上并不适合以文字整理&#xff0c;但还是决定尽量整理一份笔…...

破解路内监管盲区:免布线低位视频桩重塑停车管理新标准

城市路内停车管理常因行道树遮挡、高位设备盲区等问题&#xff0c;导致车牌识别率低、逃费率高&#xff0c;传统模式在复杂路段束手无策。免布线低位视频桩凭借超低视角部署与智能算法&#xff0c;正成为破局关键。该设备安装于车位侧方0.5-0.7米高度&#xff0c;直接规避树枝遮…...

LCTF液晶可调谐滤波器在多光谱相机捕捉无人机目标检测中的作用

中达瑞和自2005年成立以来&#xff0c;一直在光谱成像领域深度钻研和发展&#xff0c;始终致力于研发高性能、高可靠性的光谱成像相机&#xff0c;为科研院校提供更优的产品和服务。在《低空背景下无人机目标的光谱特征研究及目标检测应用》这篇论文中提到中达瑞和 LCTF 作为多…...