Linux 多线程原理深剖

目录

    • 传统艺能
    • Linux 线程
      • phread
    • 二级页表
    • 线程优点
    • 线程缺点
    • 线程异常
    • 进程与线程
    • 多线程共享
    • Linux线程控制
      • POSIX线程库
    • 线程的创建
      • 获取线程id
    • 线程等待
    • 线程终止
    • 线程分离
    • 进程地址空间布局

传统艺能

小编是双非本科大二菜鸟不赘述,欢迎米娜桑来指点江山哦
在这里插入图片描述
1319365055

非科班转码社区诚邀您入驻
小伙伴们,满怀希望,所向披靡,打码一路向北
一个人的单打独斗不如一群人的砥砺前行
这是和梦想合伙人组建的社区,诚邀各位有志之士的加入!!
社区用户好文均加精(“标兵”文章字数2000+加精,“达人”文章字数1500+加精)
直达: 社区链接点我


Linux 多线程原理深剖_第1张图片

Linux 线程

在一个程序里的一个执行路线就叫做 线程,准确的定义是 线程是一个进程内部的控制序列 \color{red} {线程是一个进程内部的控制序列} 线程是一个进程内部的控制序列

首先一切进程至少都有一个执行线程。线程在进程内部运行,本质是在进程地址空间内运行。在 Linux 中,CPU 看到的 P C B \color{red} {PCB} PCB 要比传统的进程更轻量化。透过进程虚拟地址空间可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了 线程执行流 \color{red} {线程执行流 } 线程执行流

需要明确的是,一个进程的创建实际上伴随着其进程控制块(task_struct)、进程地址空间(mm_struct)以及页表的创建:
Linux 多线程原理深剖_第2张图片
每个进程都有自己独立的进程地址空间和页表,这代表着运行本身就具有独立性。

但是我们在创建进程时,创建 task_struct ,并要求只创建 task_struct 和父 task_struct 的共享地址空间和页表:

Linux 多线程原理深剖_第3张图片

以上动作其实就是在执行四个线程:每一个线程都是当前进程的一个执行流,也就是我们理解的 “执行分支” \color{red} {“执行分支” } 执行分支,这里也很容易看出,线程运行的本质就是在这个进程地址空间运行,也就是这个进程的所有资源几乎是所有线程共享的

单纯从技术角度,这是一定能实现的,因为线程粒度比进程更小,所以它比创建原始进程更轻松。

那么再来重新理解一下之前的进程概念:

Linux 多线程原理深剖_第4张图片
因此,进程不仅通过task_struct来衡量,还要有进程地址空间、文件、信号等等,这些合起来称为一个进程,从内核角度来看 进程就是系统资源分配的基本单位 \color{red} {进程就是系统资源分配的基本单位 } 进程就是系统资源分配的基本单位,直白的说就是我们是创建一个 task_struct 并且创建进程地址空间,维护页表,然后再物理内存中开辟空间,映射地址,打开相关文件和注册信号对应的解决方案等等

而我们之前接触到的进程都只有一个 task_struct,也就是该进程内部只有一个执行流,即单执行流进程,反之,内部有多个执行流的进程叫做多执行流进程

那么问题来了:CPU 是否识别当前调度的 task_struct 是进程还是线程?

No!答案是不能,而且也不需要,CPU 只会关心一个独立的执行流,无论进程内部是一个还是多个执行流,CPU 都是以 task_struct 为单位进行来调度的,不妨对比一下单执行流和多执行流:

单执行流
Linux 多线程原理深剖_第5张图片
多执行流
Linux 多线程原理深剖_第6张图片
这就是就是我们所说的CPU看到的还是 task_struct ,只是更加轻量化。

但是!Linux 里并不存在真正的多线程,他的多线程是用进程模拟的

操作系统中存在大量的进程,一个进程内又存在多个线程,因此线程的数量一定远超进程。如果一个操作系统要真正的支持线程,就必定有某种结构对线程进程管理,比如线程的创建,终止,转换,调度和释放回收等等,为什么说 Linux 是神Linux 相比另起炉灶去创建一套线程管理体系,他选择直接就地取材用进程搭建一条平行的线程管理机制

其实这就是一个复用的思想,因此在 Linux 看来线程的控制块和进程的控制块是类似的,他并没有单独搞出一个数据结构来描述他,而是直接对进程取而用之,这也是为什么我们将所有进程·的执行流都叫做轻量化进程,Windows 是支持线程操作的,这也是为什么windows操作系统逻辑比 Linux 复杂的多

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

但是 Linux 提供了创建轻量级进程的接口,其中最典型的代表就是 vfork 函数,函数功能是创建子进程,并且父子共享空间:

pid_t vfork(void);

vfork 返回值与 fork 一样,父进程返回子进程的 PID,子进程返回 0,比如下面代码中父进程使用 vfork 创建子进程,子进程修改了全局变量 g_val,父进程休眠 3 秒后会读取到全局变量 g_val 的值证明父子进程共享地址空间:

#include 
#include 
#include 
#include 
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;
}

在这里插入图片描述

phread

phread 原生线程库,虽然在内核角度没有对应的接口可以调用,但是在用户角度,我们在实际操作是会更希望使用 thread_create 这样的接口而不是 vfork 函数,因此系统提供了原生线程库这个原生线程库其实就是对轻量级线程进行了封装然后再用户层模拟实现了一套相关的接口。

所以现在我们的根本目标并非是学习操作系统的接口,而是学习这套用户层模拟实现的接口

二级页表

众所周知 32 位平台下有 2^32 个地址,这就代表着有 2^32 个地址需要映射。我们说过页表就是一个简单的表结构,那么一张表就要有 2^32 个映射关系:
Linux 多线程原理深剖_第7张图片
每一张表的内容除了映射关系之外,还包括一些权限信息,比如页表分为了内核级页表和用户级页表,这就是通过权限信息来进行区分的:

Linux 多线程原理深剖_第8张图片
每个表项中存储了一个物理地址和一个虚拟地址,这里需要消耗 8 字节,,考虑到权限相关信息,这里粗略按照 10 字节计算。2^32 个字节,我们就需要 2^32 * 10 个字节,也就是 40 GB,但是在 32 为平台下只有最多 4 GB空间, 也就是要存储这样一张表是不可能的 \color{red} {也就是要存储这样一张表是不可能的} 也就是要存储这样一张表是不可能的

所以不能直接将页表就看成一个单纯的表,在 32 位平台下,页表映射过程是这样的:

  1. 虚拟地址前 10 个比特位在页目录中进行查找,找到对应的页表
  2. 再选择 10 个比特位在对应页表中进行查询,找到物理内存中对应页框的其实地址
  3. 将最后剩余 12 个比特位作为偏移量从页框对应地址处向后偏移,找到物理内存中的某一个对应的字节数据

物理地址也绝对不是饼粘一团的,它被划分为了一个个 4 kb大小的页框的,磁盘上的程序又会被分成一个个 4 kb 大小的页帧,当内存和磁盘进行数据交换时也是以 4 kb为单位的。不难发现,其实 4 kb就是 2^12 个字节,也就是说一个页框中会有 2^12 个字节,访问内存的基础大小是 1 字节,所以最多可以有 2^12 个地址,最后 12 为作为偏移量查找即可,从而找到物理内存中某一个对应字节数据

Linux 多线程原理深剖_第9张图片

这里就是一个二级页表结构,页目录就是一个一级页表,而表项是二级页表,每一个表大小是 10 字节,页目录和页表的表项都是 210 个,因此一个表的大小就是 210 * 10 个字节,也就是10 KB。而页目录有 210 个表项也就意味着页表有 210 个,也就是说一级页表有1张,二级页表有210张,总共算下来大概就是 10 MB,内存消耗不会太高,因此 Linux 中实际就是这样映射的

所有的映射过程都是由 M M U \color{red} {MMU} MMU 这个硬件完成的,该硬件集成在CPU内。页表是一种软件映射,MMU是一种硬件映射,所以计算机进行虚拟地址到物理地址的转化采用的是软硬件结合的方式

在 Linux 中,32位平台下用的是二级页表,而64位平台下用的是多级页表这就可以解释一下为什么修改字符串常量会出发段错误:我们修改一个常量虚拟必须找到对应的物理地址,但是查表时发现它是只读的,此时就会在 MMU 触发硬件错误,操作系统识别到报错后,就会发送信号对齐进行终止。

线程优点

  1. 创建一个线程的代价远比一个进程小的多
  2. 相比进程之间的切换,线程之间的切换所需要的系统操作会少很多
  3. 线程占的资源比进程少得多
  4. 充分利用处理器的可并行数量
  5. 在等待慢速IO结束的同时,程序可执行其他的计算任务
  6. 计算密集型任务,在多处理器上运行,会分成多个线程运行
  7. IO密集行任务,将IO操作重修,可提高效率,线程可以同时等待不同的IO操作

线程缺点

  1. 性能损失,如果计算密集型线程的任务比可用的处理器多,会产生较大的性能损失,性能包括额外的同步和调度开销,但是可用资源是不变的
  2. 健壮性降低,多线程需要全面的思考,因时间分配上的细微偏差或者因共享了不该共享的变量会造成不良影响,也就是说线程之间是缺乏保护的
  3. 缺乏访问控制,进程是访问控制的基本粒度,线程中调用 os 函数会对整个进程产生影响
  4. 难度提高,调试和编写都涉及更复杂

线程异常

比如一个线程出现除0,野指针问题,会直接全部垮掉;且线程是进程的执行分支名,线程出现异常,就像进程出现异常一样,会触发信号机制,此时所有的线程会全部退出。因此合理使用多线程,能提高 CPU 密集型程序的执行效率与用户体验

进程与线程

进程是分配系统资源的基本单位,线程是系统调度的基本单位
Linux 多线程原理深剖_第10张图片

线程共享进程数据,但也拥有自己的一部分数据,比如线程ID,线程栈(每个线程都有临时的数据,需要压栈出栈),全局变量 errno,信号屏蔽字与调度优先级

多线程共享

同一个地址空间里面,代码段和数据段和数据段都是共享的,如果定义一个函数在各线程中都可以调用。如果定义一个全局变量,在各线程中都可以访问到

各线程还共享一些进程资源和环境,文件描述符表(进程打开一个文件后,其他线程也能够看到)每种信号的处理方式,用户ID和组ID

Linux线程控制

POSIX线程库

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

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

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

线程的创建

创建线程用到 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:传给线程例程的参数

当一个程序启动时,就有一个进程被操作系统创建,与此同时一个线程也立刻运行,这个线程就叫做主线程,主线程产生其他子线程,主线程一般最后要完成某些操作,比如各种关闭动作

主线程调用 pthread_create 创建一个新线程,此后新线程会跑去执行自己的代码,而主线程则继续执行后续代码:

#include 
#include 
#include 

void* Routine(void* arg)
{
	char* msg = (char*)arg;
	while (1){
		printf("I am %s\n", msg);
		sleep(1);
	}
}
int main()
{
	pthread_t tid;
	pthread_create(&tid, NULL, Routine, (void*)"thread 1");
	while (1){
		printf("I am main thread!\n");
		sleep(2);
	}
	return 0;
}

运行代码后,可以看到新线程每隔一秒就会执行一次打印操作,但是主线程每隔两秒执行一次打印操作:

Linux 多线程原理深剖_第11张图片
使用ps -aL命令可查看当前的轻量级进程(不带 -L 是一个个的进程;带 -L 是每个进程内的多个轻量级进程)

Linux 多线程原理深剖_第12张图片
这里的 LWP 就是轻量级进程 ID,可以看到显示的两个轻量级进程的 PID 相同,因为它们属于同一个进程。

注意 \color{red} {注意} 注意在应用层现场其实就是 LWP,操作系统实际调度的是 LWP 而并非 PID,之前因为我一直是作为单线程进程在写,所以 PID 和 LWP 是相等的,所以单线程下调度谁都可以

上面是主线程创建一个新线程,我们让主线程一次创建多个新线程,并让每一个新线程都去执行同一个函数,也就是说该函数是会被重入的:

#include 
#include 
#include 
#include 
#include 

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;
}

这里就可以看到 5 个创建成功的线程:

Linux 多线程原理深剖_第13张图片
因为都属于同一个进程,所以他们的 PID 和 PPID 也都是一样的,此时使用 ps -aL 就能看到 6 个轻量级进程:

Linux 多线程原理深剖_第14张图片

获取线程id

我们可以通过 pthread_self 函数获取线程 id,类似于 getpid 获取当前进程ID:

pthread_t pthread_self(void);

下面代码中在每一个新线程被创建后,主线程都将通过输出型参数获取到的线程ID进行打印,此后主线程和新线程又通过调用 pthread_self 函数获取线程ID进行打印:

#include 
#include 
#include 
#include 
#include 

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());
		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]);
	}
	while (1){
		 printf("I am main thread...pid: %d, ppid: %d, tid: %lu\n", getpid(), getppid(), pthread_self());
		 sleep(2);
	}
	return 0;
}

Linux 多线程原理深剖_第15张图片
pthread_self 获得的线程ID与内核的 LWP 的值是不相等的,pthread_self 获得的是用户级原生线程库的线程ID,而 LWP 是内核的轻量级进程ID

线程等待

需要明确的是,一个线程被创建出来就如同进程一样,也是需要被等待的。如果主线程不对新线程进行等待,资源也是不会被回收的。所以线程需要被等待,如果不等待会产生类似于“僵尸进程”的内存泄漏问题。

等待线程函数 pthread_join:

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

thread:被等待线程的ID,retval:线程退出时的退出码信息,线程等待成功返回0,失败返回错误码

retval

  1. 如果thread线程通过return返回,retval 所指向的单元里存放的是thread线程函数的返回值。
  2. 如果thread线程被别的线程调用 pthread_cancel 异常终止掉,retval 所指向的单元里存放的是常数PTHREAD_CANCELED
  3. 如果thread线程是自己调用 pthread_exit 终止的,retval 所指向的单元存放的是传给 pthread_exit 的参数。
  4. 如果对thread线程的终止状态不感兴趣,可以传 NULL 给 retval 参数

grep命令进行查找,可以发现PTHREAD_CANCELED实际上就是头文件里面的一个宏定义,它本质就是 -1:

grep -ER "PTHREAD_CANCELED" /usr/include/

在这里插入图片描述
下面代码中我们先不关心线程退出信息,直接将 pthread_join 的第二次参数设置为NULL,等待线程后打印该线程的编号以及线程ID:

#include 
#include 
#include 
#include 
#include 

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++;
	}
	return NULL;
}
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++){
		pthread_join(tid[i], NULL);
		printf("thread %d[%lu]...quit\n", i, tid[i]);
	}
	return 0;
}

Linux 多线程原理深剖_第16张图片
线程退出时如何获取退出码呢?我们将线程退出时的退出码设置为某个特殊的值,并在成功等待线程后将该线程的退出码进行输出

#include 
#include 
#include 
#include 
#include 

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++;
	}
	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;
}

Linux 多线程原理深剖_第17张图片
pthread_join 默认是以阻塞的方式进行线程等待的,那么问题来了:为什么线程退出时只能拿到线程的退出码?这是什么问题呢?比如我们可以通过 wait 或是waitpid 的输出型参数 status,获取到退出进程的退出码、退出信号以及core dump标志。就是说只能拿到退出码难道线程就不会出现异常吗?

线程当然也会出现异常,线程退出的情况有三种:

  1. 代码运行完毕,结果正确
  2. 代码运行完毕,结果不正确
  3. 代码异常终止

因此我们也需要考虑线程异常终止的情况,但是 pthread_join 无法获取线程异常退出的信息。因为线程是进程内的一个执行分支,如果进程中的某个线程崩溃了,那么整个进程也会因此而崩溃,此时我们根本没办法执行 pthread_join,因为整个进程已经退出了

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

#include 
#include 
#include 
#include 
#include 

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;
}

结果如下,程序直接暴毙了:

Linux 多线程原理深剖_第18张图片
所以 pthread_join 只能获取到线程正常退出时的退出码,用于判断线程的运行结果是否正确。

线程终止

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

  1. 线程函数处进行return。
  2. 线程可以自己调用pthread_exit函数终止自己。
  3. 一个线程可以调用pthread_cancel函数终止同一进程中的另一个线程。

线程函数处进行 r e t u r n \color{red} {线程函数处进行return} 线程函数处进行return

注意在线程中使用return代表当前线程退出,但是在main函数中使用return代表整个进程退出

#include 
#include 
#include 
#include 
#include 

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());
		sleep(1);
	}
	return (void*)0;
}
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());

	return 0;
}

运行代码,并不能看到新线程执行的打印操作,因为主线程的退出导致整个进程退出了

Linux 多线程原理深剖_第19张图片

p t h r e a d e x i t 函数 \color{red} {pthread_exit函数} pthreadexit函数

void pthread_exit(void *retval);

retval:线程退出时的退出码信息,该函数无返回值,跟进程一样结束时无法返回自身。

pthread_exit 或者 return 返回的指针所指向的内存单元必须是全局的或者是 malloc 分配的,不能在线程函数的栈上分配,因为当其他线程得到这个返回指针时,线程函数已经退出了

这里我们使用 pthread_exit 终止线程,并将退出码设置为6666:

#include 
#include 
#include 
#include 
#include 

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++;
	}
	pthread_exit((void*)6666);
}
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;
}

Linux 多线程原理深剖_第20张图片
p t h r e a d c a n c e l 函数 \color{red} {pthread_cancel函数} pthreadcancel函数

int pthread_cancel(pthread_t thread);

这个函数代表着即线程是可以被取消的,thread 就是被取消线程的ID,线程取消成功返回0,失败返回错误码。线程是可以取消自己的,甚至新线程也可以取消主线程,取消成功的线程的退出码一般是 -1

#include 
#include 
#include 
#include 
#include 

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++;
		pthread_cancel(pthread_self());
	}
	pthread_exit((void*)6666);
}
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_exit 前将线程取消的,所以其退出码不是我们设置的 6666 而是 -1:

Linux 多线程原理深剖_第21张图片

线程分离

我们说了因为线程是进程内的一个执行分支,如果进程中的某个线程崩溃了,那么整个进程也会因此而崩溃,此时我们根本没办法执行 pthread_join,因为整个进程已经退出了

默认情况下,新创建的线程是 joinable 的,线程退出后,需要对其进行 pthread_join操作

一个线程如果被分离了,这个线程依旧要使用该进程的资源,依旧在该进程内运行,甚至这个线程崩溃了一定会影响其他线程,只不过这个线程退出时不再需要主线程去join了,当这个线程退出时系统会自动回收该线程所对应的资源。

可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离。joinable 和分离是冲突的,一个线程不能既是 joinable 又是分离的

分离函数 pthread_detach:

int pthread_detach(pthread_t thread);

thread:被分离线程的ID

线程分离成功返回0,失败返回错误码,下面创建五个新线程后让这五个新线程将自己进行分离,那么此后主线程就不需要在对这五个新线程进行 join 了

#include 
#include 
#include 
#include 
#include 

void* Routine(void* arg)
{
	pthread_detach(pthread_self());
	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++;
	}
	pthread_exit((void*)6666);
}
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]);
	}
	while (1){
		printf("I am main thread...pid: %d, ppid: %d, tid: %lu\n", getpid(), getppid(), pthread_self());
		sleep(1);
	}
	return 0;
}

Linux 多线程原理深剖_第22张图片

进程地址空间布局

pthread_create 会产生一个线程ID,存放在第一个参数指向的地址中,该线程ID和内核中的LWP不是一回事。

内核中的 LWP 属于进程调度的范畴,因为线程是轻量级进程,是操作系统调度器的最小单位,所以需要一个数值来唯一表示该线程。

pthread_create 第一个参数指向一个虚拟内存单元,该内存单元的地址即为新创建线程的线程ID,这个ID属于NPTL线程库的范畴,线程库的后续操作就是根据该线程ID来操作线程的。

线程库NPTL提供的 pthread_self 获取的线程ID 和 pthread_create 函数第一个参数获取的线程ID是一样的

所以pthread_t到底是什么类型呢

首先,Linux 没有真正的线程,只提供LWP,也就意味着操作系统只需要对内核执行流 LWP 进行管理,通过 ldd 我们可以看到当前的线程库就是一个动态库

Linux 多线程原理深剖_第23张图片
进程运行时动态库被加载到内存,然后通过页表映射到进程地址空间中的共享区,此时该进程内的所有线程都是能看到这个动态库的:

Linux 多线程原理深剖_第24张图片

我们说每个线程都有自己私有的栈,其中主线程采用的栈是进程地址空间中原生的栈,而其余线程采用的栈就是在共享区中开辟的。除此之外,每个线程都有自己的struct pthread,当中包含了对应线程的各种属性;每个线程还有自己的线程局部存储,当中包含了对应线程被切换时的上下文数据

每一个新线程在共享区都有这样一块区域对其进行描述,因此我们要找到一个用户级线程只需要找到该线程内存块的起始地址,然后就可以获取到该线程的各种信息:

Linux 多线程原理深剖_第25张图片
上面我们所用的各种线程函数,本质都是在库内部对线程属性进行的各种操作,最后将要执行的代码交给对应的内核级LWP去执行就行了,也就是说线程数据的管理本质是在共享区的

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

你可能感兴趣的:(Linux,C++,linux,网络,多线程)