前言:
承接上一篇 System V IPC 三大进程间通信机制,多进程模型实现了任务并发,但进程间切换开销大、通信成本高,在高频并发场景下并非最优解。本篇引入更轻量的并发执行单元 —— 线程,讲解 Linux 线程的底层本质、POSIX 线程库的创建与回收、线程与进程的资源差异,是理解多线程并发、后续线程同步机制的前置基础,也是笔试面试的核心必考内容。
一、线程核心概念
1. 什么是线程
进程是操作系统分配资源的基本单位,而线程是 CPU 调度和执行的基本单位。一个进程内可以包含一个或多个线程,所有线程共享所属进程的地址空间与系统资源,仅保留自身独立的执行上下文与栈空间。
Linux 系统中没有专门的 “线程” 内核结构体,线程本质是轻量级进程(LWP,Light Weight Process),内核同样用task_struct描述,和进程共用同一套调度框架,核心区别在于是否共享地址空间与系统资源。
2. 进程与线程核心对比
| 对比维度 | 进程 | 线程 |
|---|---|---|
| 本质定位 | 资源分配的基本单位 | CPU 调度的基本单位 |
| 地址空间 | 拥有独立完整的虚拟地址空间 | 无独立地址空间,共享所属进程的地址空间 |
| 切换开销 | 大,需切换页表、刷新 TLB、替换进程资源 | 小,仅切换寄存器、栈等私有上下文 |
| 通信方式 | 需要管道、共享内存等 IPC 机制 | 直接通过全局变量、堆内存交换数据 |
| 稳定性 | 进程间相互独立,一个崩溃不影响其他 | 一个线程异常崩溃,整个进程终止 |
| 创建销毁 | 速度慢,资源占用高 | 速度快,资源占用极低 |
3. 多线程的优劣势
优势
- 线程上下文切换开销远低于进程,高并发场景下 CPU 利用率更高
- 同进程内线程通信成本极低,无需内核中转,直接读写共享内存
- 创建、销毁速度快,适合短任务高频并发的场景
劣势
- 稳定性弱,单个线程触发内存错误会导致整个进程崩溃
- 多线程并发访问共享资源需要同步处理,增加编程复杂度
- 调试与问题排查难度远高于单进程程序
二、POSIX 线程库:pthread 基础
Linux 标准线程库为 pthread(POSIX Thread),属于用户态库,底层封装了内核 clone 系统调用,编译时需要链接-lpthread库。
1. 线程创建:pthread_create
#include <pthread.h> int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);thread:传出参数,保存创建成功的线程 ID(进程内唯一)attr:线程属性结构体,传NULL使用默认属性start_routine:线程入口函数,格式为void* 函数名(void*)arg:传递给线程入口函数的参数- 返回值:成功返回
0,失败直接返回错误码(不设置 errno)
注意:
pthread_t是库层面的线程 ID,和内核中的 LWP 号不是同一个概念,仅在当前进程内有效。
2. 线程退出:pthread_exit
void pthread_exit(void *retval);- 功能:主动终止当前线程,
retval为退出状态,可被其他线程通过pthread_join获取 - 与
return的区别:return只是函数返回,仅在线程主入口函数中 return 才会退出线程pthread_exit在任意函数层级调用,都会直接终止当前线程- main 函数中 return 会终止整个进程,所有线程一同退出;主线程调用
pthread_exit只会退出主线程,子线程继续运行
核心坑点:不能返回线程栈上局部变量的地址,线程退出后栈空间会被回收,成为野指针。返回值必须使用全局变量或堆内存。
3. 线程回收:pthread_join
int pthread_join(pthread_t thread, void **retval);- 功能:阻塞等待指定线程退出,回收其资源,等价于进程的
waitpid retval:传出参数,接收线程的退出状态- 每个非分离线程必须被 join 一次,否则会残留线程资源,造成资源泄漏
4. 线程分离:pthread_detach
int pthread_detach(pthread_t thread);- 功能:将指定线程设置为分离状态,线程退出后由系统自动回收资源,无需手动 join
- 分离后的线程无法再被 join,也无法获取其退出返回值
- 适用于不需要关心返回值的后台常驻线程
5. 线程取消:pthread_cancel
int pthread_cancel(pthread_t thread);- 功能:向指定线程发送取消请求,请求其终止执行
- 注意:这是请求而非强制终止。默认情况下线程会在取消点(如系统调用、IO 操作)响应退出;线程也可以设置取消状态,忽略取消请求。
三、线程的共享资源与私有资源(面试高频)
线程共享进程的大部分资源,但也保留自身独立的执行上下文,明确两者边界是理解多线程行为的核心。
| 资源类别 | 线程间共享 | 线程私有 |
|---|---|---|
| 内存空间 | 代码段、全局数据段、堆内存、共享映射区 | 线程栈、线程局部存储(TLS) |
| 系统资源 | 文件描述符表、信号处理函数配置 | 寄存器上下文、程序计数器 |
| 进程属性 | 用户 ID / 组 ID、工作目录、umask 掩码 | 线程 ID、errno 变量、信号屏蔽字 |
| 调度相关 | 进程优先级基准 | 线程独立调度优先级 |
关键细节:
errno是每个线程独立的变量,多线程下不会互相干扰;但文件描述符完全共享,一个线程修改文件偏移量,其他所有线程都会受影响。
四、实战:多线程创建与回收
#include <stdio.h> #include <pthread.h> #include <unistd.h> #include <stdlib.h> // 线程1:接收参数,计算后返回堆上的结果 void *thread_task1(void *arg) { int num = *(int *)arg; printf("线程1启动,参数:%d,线程ID:%lu\n", num, pthread_self()); int *result = malloc(sizeof(int)); *result = num * 2; sleep(1); pthread_exit(result); } // 线程2:简单任务执行后退出 void *thread_task2(void *arg) { printf("线程2启动,线程ID:%lu\n", pthread_self()); sleep(2); printf("线程2执行完毕\n"); pthread_exit(NULL); } int main(void) { pthread_t tid1, tid2; int param = 10; // 创建两个子线程 int ret = pthread_create(&tid1, NULL, thread_task1, ¶m); if (ret != 0) { fprintf(stderr, "线程1创建失败\n"); return 1; } ret = pthread_create(&tid2, NULL, thread_task2, NULL); if (ret != 0) { fprintf(stderr, "线程2创建失败\n"); return 1; } printf("主线程运行,等待子线程回收\n"); // 回收线程1并获取返回值 void *ret_val; pthread_join(tid1, &ret_val); printf("线程1已回收,计算结果:%d\n", *(int *)ret_val); free(ret_val); // 回收线程2 pthread_join(tid2, NULL); printf("线程2已回收,程序结束\n"); return 0; }编译命令:gcc demo.c -o demo -lpthread
五、Linux 线程底层本质:轻量级进程
Linux 的线程实现属于内核级线程,和 Windows 的用户态线程不同:
- 底层通过
clone系统调用创建,和fork同源,区别在于 clone 可以指定共享哪些资源 - 创建线程时,共享地址空间、文件描述符表、信号处理表等进程资源,仅分配独立的栈与寄存器上下文
- 内核调度器对进程和线程一视同仁,都作为独立调度单元参与 CPU 调度
这种实现的优势是线程由内核调度,一个线程阻塞不会影响同进程的其他线程;劣势是线程操作都需要系统调用,有一定开销。
六、面试高频考点与易错坑点
1. 经典面试问答
Q1:进程和线程有什么本质区别?
答:
- 进程是操作系统分配资源的基本单位,线程是 CPU 调度的基本单位。
- 进程拥有独立的虚拟地址空间,线程没有独立地址空间,共享所属进程的地址空间与系统资源。
- 进程切换开销大,需要切换页表与进程资源;线程切换开销小,仅切换私有上下文。 进程间通信需要 IPC 机制,线程间可直接通过共享内存通信。
Q2:为什么线程切换的开销远小于进程?
答: 进程切换需要替换整个地址空间的页表、刷新 TLB 缓存,同时切换文件描述符表、信号处理等进程资源; 而线程共享进程的地址空间,切换时只需要保存和恢复线程的寄存器、栈指针等私有执行上下文,不需要切换地址空间,因此开销小很多。
Q3:pthread_join 和 pthread_detach 有什么核心区别?
答: pthread_join 是阻塞等待指定线程退出,手动回收线程资源,同时可以获取线程的退出返回值; pthread_detach 是将线程设置为分离状态,线程退出后由系统自动回收资源,不需要手动 join,也无法获取返回值。
Q4:线程退出时,return 和 pthread_exit 有什么区别?
答: 在线程入口主函数中,两者效果一致,都会终止当前线程。 但 return 只是函数返回,在线程调用的子函数中 return 只会返回到上一层,不会退出线程;pthread_exit 在任意层级调用都会直接终止当前线程。 另外 main 函数中 return 会终止整个进程,所有线程一同退出;主线程调用 pthread_exit 只会退出主线程,子线程继续运行。
Q5:线程之间哪些资源共享,哪些私有?
答: 共享资源:代码段、全局数据、堆内存、文件描述符表、信号处理方式、用户 ID 与组 ID、工作目录。 私有资源:线程 ID、线程栈、寄存器上下文、errno、信号屏蔽字、线程局部存储。
2. 常见易错坑点
- 线程退出返回栈上局部变量的地址,线程退出后栈空间回收,导致野指针,必须使用堆内存或全局变量传递返回值
- 编译 pthread 程序忘记添加
-lpthread链接选项,出现函数未定义报错 - 非分离线程忘记调用 pthread_join 回收,造成线程资源泄漏,耗尽进程线程数上限
- 误以为 pthread_self 返回的线程 ID 就是内核 LWP 号,两者分属不同层面,并不等价
- 多线程并发修改全局变量不加同步保护,导致数据竞争、计算结果异常
- main 函数直接 return 退出,误以为子线程还能继续运行,实际整个进程会终止,所有线程被销毁
- 对已分离的线程重复调用 pthread_join,导致调用失败
以上就是 Linux 线程基础的全部核心内容,线程是高并发编程的基础执行单元,但共享资源带来的数据竞争问题必须通过同步机制解决。下一篇我们将讲解线程同步的三大核心工具:互斥锁、条件变量与读写锁,拆解死锁成因与规避方案。
制作不易,如果对你有用,希望能点赞收藏支持一下。