前言

在并发编程的世界里,线程是实现高效资源利用与任务并行的核心载体。作为进程内的独立执行分支,线程既共享进程的全局资源,又拥有专属的执行上下文,这种特性让它成为平衡 “性能开销” 与 “并发效率” 的理想选择。尤其在 Linux 系统中,线程通过轻量级进程(LWP)的形式被巧妙模拟,其底层与内核调度、地址空间的关联的设计,既体现了操作系统的精妙,也为开发者带来了需要深入理解的技术细节。

本文将从线程的本质出发,带大家层层拆解 Linux 多线程的核心逻辑:先明确线程与进程的区别与联系,剖析线程共享资源与私有数据的边界;再深入 Linux 内核的实现机制,理解轻量级进程如何通过task_struct模拟线程,以及地址空间、页表在线程调度中的作用;随后聚焦实战,详细讲解 POSIX 线程库(pthread)的核心接口 —— 从线程创建(pthread_create)、等待回收(pthread_join),到线程分离(pthread_detach)、退出(pthread_exit),每个接口都搭配使用场景与注意事项;最后还会探讨线程异常的影响、线程局部存储(__thread)等关键知识点。

无论你是刚接触并发编程的新手,还是想夯实 Linux 多线程底层基础的开发者,本文都能为你提供清晰的学习路径:从理论原理到实战接口,从核心特性到避坑指南,帮助你彻底搞懂 Linux 多线程的设计思想与编程技巧,为后续实现复杂的并发场景(如线程同步、互斥锁应用)打下坚实基础。

线程的简介

线程是进程内的一个执行分支,是操作系统调度的基本单位

进程是承担分配系统资源的基本实体,线程是承载执行流的载体

注意:执行流不是资源!执行流想要执行的话,需要资源

线程会去共享进程的数据,但是也有一部分是自己的数据:

1.线程ID 2.一组寄存器(存线程的上下文) 3.栈 4.errno 5.信号屏蔽字 6.调度优先级

存线程的上下文和栈是最重要的

注意:堆区是共用的哈

Linux下的线程

Linux没有真正意义上的线程,而是用轻量级进程的PCB模拟的线程

也就是线程就是用task_struct去把进程的资源给分配一部分过来

CPU角度看,执行粒度:进程>=执行流>=线程

执行流还能>线程的原因:执行流可以是进程或者线程

CPU只有调度执行流的概念,分不清是进程还是线程;但是操作系统可以

线程的执行粒度会比进程要细

重新理解地址空间:

1.进程的概念:是一部分物理内存+内核数据结构+页表+地址空间(地址空间是进程的资源窗口)

(进程最初创建的那个task_struct所属的线程叫做主线程)

2.页表的重新理解

虚拟地址转换成物理地址:

虚拟地址一共32位 分成10+10+12

高10位转换成十进制数就是页目录里面的下标,中间10位转换过后是二级页表的下标–然后就找到了所属的页框的起始地址,再加上偏移量(后12位),就得到了这个数据的起始地址在哪了

在这里插入图片描述
每格叫做表项

引申:1.一个页框一般是4KB ,页是内存管理的最小分配单元

   2.页目录的各个位置一般都是存了东西的,但是二级页表大部分情况下是不全的(需要用的时候才给开辟空间)

引申:如果想查询系统的执行流情况

可以用指令ps -aL

在这里插入图片描述

这行的LWP表示线程的pid

如果PIDLWP一样的话就说明是主线程

注意:程序替换替换的是一整个程序,那些线程会通通换掉的

线程和进程的比较

线程的优点:

1.创建一个线程比创建一个进程的代价小得多

2.线程间切换的代价比进程间切换的代价小

3.能充分利用多处理器的可并行数量

4.在等待慢速I/O操作结束的同时,程序可执行其他的计算任务–线程更容易

5.计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现

6.I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。

线程的缺点:

1.性能损失 2.因为缺乏访问控制,导致健壮性降低 3.编程难度大些

线程比进程更加轻量化的原因:

1.创建和释放更轻量化

2.切换更轻量化–因为线程切换不需要重新cache数据

其实线程切换上下文的代价也是不小的

线程异常

线程出异常的话是会连坐进程的

比如:10/0的话,整个进程都会报错

或者kill -9 LWP的值,会把整个进程都杀掉

所以pthread_join那个函数根本不需要考虑线程异常了返回什么

线程库

线程库会维护线程的概念,也就是把线程的属性先描述再组织存储好

对于动态库的话,加载到内存的共享区后,把线程的属性放库里面

–除了主线程,其他的线程的独立栈都放在共享区了;每一个线程库级别的tcb起始地址,叫做线程的tid

线程的tcb在这里插入图片描述

线程会复用进程的全局变量,文件描述符等资源哈

–也就是说,全局变量可以被主线程和其他线程访问

虽然每个线程都有自己独立的栈结构,但是线程的栈上的数据,是可以被其他线程看到并访问的

引申:线程局部存储区:

在变量前面加上__thread,这个变量就会到这个区(只能用来定义内置类型哈)

eg: __thread int i = 0;

这样的话,这个变量在不同线程里面就互不干扰了–跟全局变量的区别

                但是一个线程里面的这个变量在这个线程的任何位置都能用--跟局部变量的区别

也就是相当于是一个私有的全局变量

引申:线程在执行时,把共享数据加载到CPU寄存器的本质:

把数据的内容变成自己的上下文–也就是以拷贝的方式,给自己搞了独立的一份

关于线程的一些接口

Linux只提供了轻量级进程的系统调用接口–但是使用起来很麻烦,而且可移植性差,所以一般都用的语言封装过的接口(简单,而且可移植性强)

pthread库

这是C语言的第三方库–但是基本上所有的Linux平台都是默认自带这个库的–类unix才能用,windows不行

但是比如C++11有自己的线程库,这个的话可跨平台性就很强–在linux下,底层封装的是pthread,所以链接时也要-lpthread

注意:这个库是第三方库,链接时需要显示链接

eg:g++ -o text text.cpp -std=c++11 -lpthread

pthread_create

在这里插入图片描述

这个可以用来创建线程

返回值:成功就返回0;失败的话返回非0值(该数表示出错的原因),不会设置errno

参数:

thread:输出型参数,这个指针指向的位置会存线程的id

attr:线程的属性,这个不用管,传NULL就行了

start_routine:是函数指针,创建的线程会去执行这个函数(函数执行完了,这个线程就退出了)

arg:给start_routine传参用的,start_routine的形参会接受arg(传对象进去都行);不需要的话,就传NULL

使用展示:
 void *threadRoutine(void *args)
 {}

pthread_create(&tid, nullptr, threadRoutine, (void*)"1234");//不强转的话可能会报错
注意点:
如果上面的args是比如:结构体指针 threadData*的话
在threadRoutine里面需要先threadData *td = static_cast<threadData *>(args);一下
不然无类型指针访问不了结构体里面
pthread_join

在这里插入图片描述

作用:是用来等待线程,然后回收线程的(主线程等待其他线程哈–所以主线程要最后再退出)

不等待进程的话,会出现类似僵尸进程的情况

返回值:成功返回0;失败的话返回非0的错误码,也不会设置errno

形参:

thread:表示想等待哪个线程,传这个线程的ID(不是LWP哈)

retval:接收被等待线程的返回值

注意:retvalvoid**类型的

比如:

void* retval;
pthread_join(pid,&retval);  如果返回值是(void*)1,就会被接收到,然后retval的值就是1

等待过程是阻塞的!不会继续往下执行

pthread_exit

在这里插入图片描述

作用:用于结束当前线程 –pthread_cancel也行,但是不常见(引申:pthread_cancel的返回值是PTHREAD_CANCELED(宏,其实是(void*)-1) )

形参:

retval:代表这个线程的返回值

还有种取消线程的方法就是return ... ;不要用exit,会把整个进程结束的

pthread_self

在这里插入图片描述

作用:获取当前线程的ID,获得的不是lwp

使用eg:auto a = pthread_self();

但是用户级线程跟内核的lwp的数量一般是1:1的关系

pthread_detach

作用:把线程跟主线程分离,分离开之后,当线程退出时,自动释放线程资源。

注意:joinable和分离是冲突的,一个线程不能既是joinable又是分离的

分离的本质其实就是修改 task_struct 中与线程分离状态相关的属性

在这里插入图片描述

这个是在主线程里面使用的

返回值:成功返回0,失败返回非0的错误码

在这里插入图片描述

这个是其他线程对自己用的,把自己跟主线程分离开来

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐