Linux 多线程编程从入门到精通:底层原理、核心接口与实践指南----《Hello Linux!》(17)
文章目录
前言
在并发编程的世界里,线程是实现高效资源利用与任务并行的核心载体。作为进程内的独立执行分支,线程既共享进程的全局资源,又拥有专属的执行上下文,这种特性让它成为平衡 “性能开销” 与 “并发效率” 的理想选择。尤其在 Linux 系统中,线程通过轻量级进程(LWP)的形式被巧妙模拟,其底层与内核调度、地址空间的关联的设计,既体现了操作系统的精妙,也为开发者带来了需要深入理解的技术细节。
本文将从线程的本质出发,带大家层层拆解 Linux 多线程的核心逻辑:先明确线程与进程的区别与联系,剖析线程共享资源与私有数据的边界;再深入 Linux 内核的实现机制,理解轻量级进程如何通过task_struct模拟线程,以及地址空间、页表在线程调度中的作用;随后聚焦实战,详细讲解 POSIX 线程库(pthread)的核心接口 —— 从线程创建(pthread_create)、等待回收(pthread_join),到线程分离(pthread_detach)、退出(pthread_exit),每个接口都搭配使用场景与注意事项;最后还会探讨线程异常的影响、线程局部存储(__thread)等关键知识点。
无论你是刚接触并发编程的新手,还是想夯实 Linux 多线程底层基础的开发者,本文都能为你提供清晰的学习路径:从理论原理到实战接口,从核心特性到避坑指南,帮助你彻底搞懂 Linux 多线程的设计思想与编程技巧,为后续实现复杂的并发场景(如线程同步、互斥锁应用)打下坚实基础。
线程的简介
线程是进程内的一个执行分支,是操作系统调度的基本单位
进程是承担分配系统资源的基本实体,线程是承载执行流的载体
注意:执行流不是资源!执行流想要执行的话,需要资源
线程会去共享进程的数据,但是也有一部分是自己的数据:
1.线程ID 2.一组寄存器(存线程的上下文) 3.栈 4.
errno5.信号屏蔽字 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如果
PID和LWP一样的话就说明是主线程
注意:程序替换替换的是一整个程序,那些线程会通通换掉的
线程和进程的比较
线程的优点:
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:接收被等待线程的返回值注意:
retval是void**类型的比如:
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的错误码

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







所有评论(0)