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

文章目录

  • 一、 线程概念
    • 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;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/282724.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Linux初识环境变量

&#x1f30e;环境变量【上】 文章目录&#xff1a; 环境变量 什么是环境变量 关于命令行参数 环境变量       简单了解       为什么需要环境变量       系统中其他环境变量 总结 前言&#xff1a; 环境变量是一种非常重要的概念&#xff0c;它们对于系统的…

长安链正式发布三周年,技术更迭支撑产业变革

导语&#xff1a; 2024年1月27日长安链正式发布三周年&#xff0c;开源社区借开年之际与大家一同回顾长安链三年来的技术发展历程&#xff0c;每一个里程碑的建设都得益于与长安链同行的合作伙伴与开发者&#xff0c;希望在2024年可以共同携手继往开来&#xff0c;为数字经济发…

Mysql面试题以及答案

1 基础 1.1、MySQL有哪些数据库类型&#xff1f; 数值类型 有包括 TINYINT、SMALLINT、MEDIUMINT、INT、BIGINT&#xff0c;分别表示 1 字节、2 字节、3 字节、4 字节、8 字节的整数类型。 1&#xff09;任何整数类型都可以加上 UNSIGNED 属性&#xff0c;表示无符号整数。 …

Matlab快捷键与函数

注释&#xff1a;注释对于代码的重要性我们就不做过多的解释了。不做注释的代码不是好代码。选中要注释的语句&#xff0c;按快捷键CtrlR,或者在命令行窗口上面的注释地方可以进行注释。当然也可以直接在语句前面“%”就可以&#xff08;注意&#xff1a;一定要用英文符号&…

第十二届蓝桥杯省赛CC++ 研究生组

十二届省赛题 第十二届蓝桥杯省赛C&C 研究生组-卡片 第十二届蓝桥杯省赛C&C 研究生组-直线 第十二届蓝桥杯省赛C&C 研究生组-货物摆放 第十二届蓝桥杯省赛C&C 研究生组-路径 第十二届蓝桥杯省赛C&C 研究生组-时间显示 第十二届蓝桥杯省赛C&C 研究生组…

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

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

Matlab 双目相机标定(内置函数)

文章目录 一、简介二、实现代码三、实现效果参考资料一、简介 相机标定的目的就是要找到从世界坐标转换为图像坐标所用到的投影P矩阵各个系数(即相机的内参与外参)。具体过程如下所述: 1、首先我们需要获取一个已知图形的图像(这里我们使用MATLAB所提供的数据)。 2、找到同…

_nodemon自动重启服务器

文章目录 1.安装模块 nodemon1.1安装方式2.jason文件里面可以存储自定义指令 由于每次修改代码都要重启服务器&#xff0c;所以我们希望有一种方式自动监视代码修改&#xff0c;自动启动服务器nodemon模块解决了这个问题 1.安装模块 nodemon 1.1安装方式 全局安装 npm i node…

计算地球圆盘负荷产生的位移

1.研究背景 计算受表面载荷影响的弹性体变形问题有着悠久的历史&#xff0c;涉及到许多著名的数学家和物理学家&#xff08;Boussinesq 1885&#xff1b;Lamb 1901&#xff1b;Love 1911&#xff0c;1929&#xff1b;Shida 1912&#xff1b;Terazawa 1916&#xff1b;Munk &…

TCP | TCP协议格式 | 三次握手

1.TCP协议 为什么需要 TCP 协议 &#xff1f;TCP 工作在哪一层&#xff1f; IP网络层是不可靠的&#xff0c;TCP工作在传输层&#xff0c;保证数据传输的可靠性。 TCP全称为 “传输控制协议&#xff08;Transmission Control Protocol”&#xff09;。 TCP 是面向连接的、可靠…

京东云开发者:DDD 学习与感悟 —— 向屎山冲锋

原文地址:https://mp.weixin.qq.com/s/Hvq1ttBopbxypatVcKcLiA 软件系统是通过软件开发来解决某一个业务领域或问题单元而产生的一个交付物。而通过软件设计可以帮助我们开发出更加健壮的软件系统。因此&#xff0c;软件设计是从业务领域到软件开发之间的桥梁。而DDD是软件设计…

opengl 学习(六)-----坐标系统与摄像机

坐标系统与摄像机 分类引言坐标系统摄像机教程在CMake中使用全局定义预编译宏,来控制是否开启错误检查补充 分类 opengl c 引言 OpenGL希望在每次顶点着色器运行后&#xff0c;我们可见的所有顶点都为标准化设备坐标(Normalized Device Coordinate, NDC)。也就是说&#xff…

OCP NVME SSD规范解读-14.Firmware固件升级要求

4.11节 Firmware Update Requirements 描述了数据中心NVMe SSD固件更新的具体要求&#xff0c;确保固件升级过程既安全又可靠&#xff0c;同时充分考虑了设备在升级过程中的可用性和功能性。 FWUP-1: 设备必须记录每一次固件激活过程。这意味着固件升级过程中&#xff0c;设备会…

【Dynamics 365 FO】在Dynamics 365中建立一个SSRS报表

建立一个SSRS报表主要有以下8个步骤&#xff1a; 目录 1、新建合约类 合约类&#xff08;Contract Class&#xff09;的作用是获取查询数据源所需要的数据&#xff0c;在我们点开报表的时候&#xff0c;系统会弹出一个对话框让我们来选择字段来筛选要查询数据&#xff0c;合…

基于Java的厦门旅游电子商务预订系统(Vue.js+SpringBoot)

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 景点类型模块2.2 景点档案模块2.3 酒店管理模块2.4 美食管理模块 三、系统设计3.1 用例设计3.2 数据库设计3.2.1 学生表3.2.2 学生表3.2.3 学生表3.2.4 学生表 四、系统展示五、核心代码5.1 新增景点类型5.2 查询推荐的…

CANoe自带的TCP/IP协议中TCP发送时的一个特殊处理(我一定是第一个发现的)

我们知道,CANoe软件中配置以太网通道后,添加的仿真节点可以作为一个主机或者一个应用来实现以太网通信。但不管是作为主机还是应用,仿真节点都需要配置TCP/IP协议栈。 有了TCP/IP协议栈,设置了网卡信息后(IP地址、MAC地址等),仿真节点就可以通过编写CAPL代码的方式发送和…

关于UDP协议

UDP协议是基于非连接的发送数据就是把数据包简单封装一下&#xff0c;然后从网卡发出去就可以&#xff0c;数据包之间没有状态上的联系&#xff0c;UDP处理方式简单&#xff0c;所以性能损耗非常少&#xff0c;对于CPU、内存资源的占用远小于TCP&#xff0c;但是对于网络传输过…

[综述笔记]A Survey on Deep Learning for Neuroimaging-Based Brain Disorder Analysis

论文网址&#xff1a;Frontiers | A Survey on Deep Learning for Neuroimaging-Based Brain Disorder Analysis (frontiersin.org) 英文是纯手打的&#xff01;论文原文的summarizing and paraphrasing。可能会出现难以避免的拼写错误和语法错误&#xff0c;若有发现欢迎评论…

visual studio卸载几种方法

1、控制面板卸载&#xff1b; 2、有时候会发现控制面板卸载会失败&#xff0c;无法卸载&#xff0c;这时候要先把下面目录的关于visual studio的都删除&#xff0c;然后重启电脑后&#xff0c;重新安装vs即可。

java Flink(四十三)Flink Interval Join源码解析以及简单实例

背景 之前我们在一片文章里简单介绍过Flink的多流合并算子 java Flink&#xff08;三十六&#xff09;Flink多流合并算子UNION、CONNECT、CoGroup、Join 今天我们通过Flink 1.14的源码对Flink的Interval Join进行深入的理解。 Interval Join不是两个窗口做关联&#xff0c;…