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

文章目录

  • 一、 线程概念
    • 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){
		//child
		g_val = 200;
		printf("child:PID:%d, PPID:%d, g_val:%d\n", getpid(), getppid(), g_val);
		exit(0);
	}
	//father
	sleep(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就是线程id
	pthread_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
		//哪个子线程调用它就输出哪个子线程的ID
		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);
		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()函数输出主线程ID
		 sleep(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;
}

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【pcolor数据可视化】Matlab vs. Python

1、Matlab代码及结果 代码 clear;clc load(.\nclcolormap.mat)sl [0,50,100,200,500,0]; el [50,100,200,500,1000,200];for i 1:length(sl)file [..\data\static_result\VIS_Min-,num2str(sl(i)),to,num2str(el(i)),_yearly.npy];data readNPY(file);maskreadNPY(.\mas…

基于springboot的mysql实现读写分离

前言: 首先思考一个问题:在高并发的场景中,关于数据库都有哪些优化的手段&#xff1f;常用的有以下的实现方法:读写分离、加缓存、主从架构集群、分库分表等&#xff0c;在互联网应用中,大部分都是读多写少的场景,设置两个库,主库和读库,主库的职能是负责写,从库主要是负责读…

FloodFill算法——图像渲染

文章目录 题目解析题目内容解读 算法解析代码解析 题目解析 首先我们先来看看题目&#xff1a;图像渲染 题目内容解读 我们来解读一下题目内容这个题目的意思其实就是有一个如下图所示的二维矩阵 这个题目的意思在这类题目中也是非常标准的&#xff0c;就是给我们一个二维数…

yaml 语法和在线解析工具

文章目录 在线解析工具1. 简介2. 语法规则3. 数据类型3.1 数组&#xff1a;3.2对象&#xff1a;3.3 标量3.4 复合结构3.5 锚点3.5.1 单个锚点3.5.6 多个锚点 3.6 引号 参考 在线解析工具 工具1 工具2 1. 简介 Yaml是一种可读性高的数据标记语言&#xff0c;Yaml文件是一种配…

python基础知识(三)基本编程题,应用题

基本编程题 1.从键盘输入一个整数和一个字符&#xff0c;以逗号隔开&#xff0c;在屏慕上显示输出一条信息。 示例如下: 输入&#xff1a; 10, 输出&#xff1a; 10 s input("请输入一个整数和一个字符&#xff0c;用逗号隔开&#xff1a;")l s.split(",&q…

使用vscode调试代码

Step1&#xff1a;在系统中安装gdb 在Ubuntu系统下安装gdb&#xff1a; apt-get update apt-get install gdb 在CentOS系统下安装gdb&#xff1a; yum install gdb Step2&#xff1a;编译生成Debug版本的可执行程序 假设源文件名称为test.cpp&#xff0c;使用g编译器&#…

你不知道的console

console console 对象提供了浏览器控制台调试的接口&#xff0c;我们可以从任何全局对象中访问到它&#xff0c;在不同浏览器上它的工作方式可能不一样&#xff0c;但通常都会提供一套共性的功能。 1.console.log() 打印内容的通用方法&#xff0c;使用方法可以参考使用字符…

DBO优化LSBoost回归预测(matlab代码)

DBO-LSBoost回归预测matlab代码 蜣螂优化算法(Dung Beetle Optimizer, DBO)是一种新型的群智能优化算法&#xff0c;在2022年底提出&#xff0c;主要是受蜣螂的的滚球、跳舞、觅食、偷窃和繁殖行为的启发。 数据为Excel股票预测数据。 数据集划分为训练集、验证集、测试集,比…

【系统架构师】-计算机网络

1、网络的划分 网络性能指标&#xff1a;速率、带宽(频带宽度或传送线路速率)、吞吐量、时延、往返时间、利用率。 网络非性能指标&#xff1a;费用、质量、标准化、可靠性、可扩展性、可升级性、易管理性和可维护性。 总线型(利用率低、干扰大、价格低)、 星型(交换机转发形…

【Linux】系统开启和关闭过程

Linux 系统启动过程 BIOS 自检&#xff1a;在计算机开机时&#xff0c;BIOS 会进行自检&#xff0c;检查硬件设备是否正常。 加载引导程序&#xff1a;BIOS 自检完成后&#xff0c;会加载引导程序&#xff0c;如 GRUB、LILO 等。引导程序会加载内核和初始化 RAM 磁盘&#xff…

数据结构:详解【栈和队列】的实现

目录 1. 栈1.1 栈的概念及结构1.2 栈的实现1.3 栈的功能1.4 栈的功能的实现1.5 完整代码 2. 队列2.1 队列的概念及结构2.2 队列的实现2.3 队列的功能2.4 队列的功能的实现2.5 完整代码 1. 栈 1.1 栈的概念及结构 栈&#xff1a;一种特殊的线性表&#xff0c;其只允许在固定的…

如何看待腾讯 QQ 浏览器抄袭 Arc

今天在 Reddit 的帖子上看到&#xff0c;QQ 浏览器抄袭了 Arc 而且还是 Arc 官方发布的 It looks very similar lol 看起来也太像了&#xff0c;笑死我了 稍微震惊了一下&#xff0c;带着疑惑&#xff0c;打开了 QQ 浏览器官网页 点击下载 ⬇️ 下载后打开 翻找了下&#xff0…

2004-2022年各省化学需氧量数据(无缺失)

2004-2022年各省化学需氧量数据&#xff08;无缺失&#xff09; 1、2004-2022年 2、范围&#xff1a;31省 3、指标&#xff1a;化学需氧量 4、来源&#xff1a;各省年鉴、国家统计局、环境年鉴 5、指标解释&#xff1a;化学需氧量(COD)排放量指工业废水中COD排放量与生活污…

java 泛型(下)

本篇文章主要说明的是类型通配符、可变参数、可变参数的使用等。 在学习之前&#xff0c;希望能对泛型有个大概了解&#xff0c;可参考链接 java 泛型&#xff08;上&#xff09;-CSDN博客 也希望对泛型类、泛型接口、泛型方法有个大概的认识及使用&#xff0c;可参考链接 j…

【保姆级教程】YOLOv8_Track多目标跟踪,快速运行

一、YOLOV8环境准备 1.1 下载安装最新的YOLOv8代码 仓库地址&#xff1a; https://github.com/ultralytics/ultralytics1.2 配置环境 pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple二、下载测试视频&#xff0c;预训练权重 测试视频 链接&am…

nuc980下 RTL8188EUS_wifi移植过程

我使用的nuc980型号为NUC980DK61YC&#xff0c;内核版本为"linux 4.4.115" &#xff0c;以下过程是在自己单片机上移植的过程&#xff0c;仅供参考&#xff0c;不同配置环境可能会有不同的坑需要踩&#xff0c;希望会对各位小伙伴有帮助。 1.驱动添加与调整 注意&a…

[综述笔记]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;若有发现欢迎评论…

NET 自定义控件

如果添加 Category&#xff0c; 自定义控件&#xff0c;会放在杂项中

03-Java面试题八股文-----java基础——10题

41、HashMap 的长度为什么是 2 的 N 次方呢&#xff1f; 为了能让 HashMap 存数据和取数据的效率高&#xff0c;尽可能地减少 hash 值的碰撞&#xff0c;也就是说尽量把数据能均匀的分配&#xff0c;每个链表或者红黑树长度尽量相等。 我们首先可能会想到 % 取模的操作来实现。…

6 修改主机名和HOSTS文件

后期我们会配置多台服务器&#xff0c;那么每台服务器我们都会给定一个主机名&#xff0c;方便后期通过主机名进行访问。主机名的修改我们可以在安装操作系统时对其修改&#xff0c;如果忘记了&#xff0c;就可以修改配置文件完成&#xff0c;像后期我们进行虚拟机克隆后&#…