Linux---线程---全面讲解
Linux 线程是系统编程的核心概念之一,作为轻量级执行单元,它在提高程序并发性能、优化资源利用率方面发挥着关键作用。本文将从底层原理、核心 API、同步机制、高级特性四个维度,结合代码示例和底层实现细节,为你系统讲解 Linux 线程知识。
线程知识点总览:
知识模块 核心内容 底层关键点
线程基础概念
1. 线程与进程的区别:线程是进程内的执行单元,共享进程资源(地址空间、文件描述符等),独立拥有栈、寄存器、线程 ID
2. Linux 线程实现:轻量级进程(LWP),通过clone()系统调用创建,共享进程资源但拥有独立的 task_struct
3. 线程库:NPTL(Native POSIX Thread Library),符合 POSIX 标准,是 Linux 默认线程实现
Linux 没有原生线程,通过进程模拟线程,内核调度的是 LWP,pthread 库封装了内核接口
线程核心操作
1. 创建:pthread_create()
2. 等待:pthread_join()(回收线程资源)
3. 终止:pthread_exit()、return、被取消
4. 分离:pthread_detach()(线程结束后自动释放资源)
pthread_t本质是 LWP 的标识符,线程创建时默认非分离状态,必须等待或分离否则资源泄漏
线程同步机制
1. 互斥锁(mutex):保护临界区,防止并发修改
2. 条件变量(condition):实现线程间等待 / 通知机制
3. 信号量(semaphore):支持多线程间的计数同步
4. 读写锁(rwlock):读共享、写独占,适合读多写少场景5. 自旋锁(spinlock):忙等待锁,适合临界区极短的场景
同步机制的底层依赖内核的 futex(快速用户空间互斥体),避免不必要的系统调用,提升性能
线程高级特性
1. 线程属性:pthread_attr_t,设置栈大小、分离状态、调度策略等
2. 线程局部存储(TLS):__thread关键字或pthread_key_create,实现线程私有数据
3. 线程安全与可重入:线程安全是多线程环境下函数的正确性,可重入是函数本身的无状态特性
TLS 的底层通过段寄存器或内核数据结构实现线程私有空间,__thread是编译器级别的实现
调度与信号
1. 线程调度:Linux CFS 调度器按时间片调度线程,支持设置线程优先级
2. 线程与信号:pthread_sigmask设置线程信号掩码,pthread_kill向指定线程发送信号
线程共享进程的信号处理函数,但拥有独立的信号掩码,信号默认只递送给任意一个线程
实践与问题
1. 死锁:避免方式(破坏四大条件、加锁顺序一致)
2. 调试工具:pstack查看线程栈,gdb的info threads调试多线程
3. 性能优化:减少锁竞争,使用无锁数据结构
多线程问题的排查核心是定位竞态条件和死锁,可通过valgrind的helgrind工具检测
学习的关键在于理解:
线程如何通过内核接口实现创建与管理
如何通过同步机制解决并发带来的资源竞争问题
线程与进程在资源共享、调度、信号处理上的差异
一、线程基础概念
1.1 线程与进程的本质区别
在 Linux 中,线程和进程的界限相对模糊,理解二者的核心差异是掌握线程的基础:
对比维度 进程 线程 通俗类比
资源分配 拥有独立的地址空间、文件描述符表等资源 共享进程的地址空间和大部分资源 进程是独立工厂,线程是工厂内的工人
调度单位 操作系统调度的基本单位(早期) 现代操作系统的基本调度单位 工厂整体不干活,工人具体执行任务
上下文切换 开销大(需切换地址空间、页表等) 开销小(仅切换寄存器、栈等) 换工厂需重新熟悉环境,换工人只需交接工具
通信方式 需借助 IPC 机制(管道、信号、套接字等) 直接读写共享内存,通信高效 不同工厂需快递通信,同工厂可直接交谈
Linux 的特殊实现:Linux 没有单独的线程调度机制,线程是通过 ** 轻量级进程(Light Weight Process, LWP)** 实现的。每个线程在内核中对应一个 task_struct(进程控制块),但多个线程共享同一个地址空间和资源。
1.2 线程的优势与适用场景
优势:
并发执行:同一进程内多个线程可同时执行,提升多核 CPU 利用率
资源共享:共享代码段、数据段、文件描述符,减少资源开销
快速切换:上下文切换开销远小于进程
通信高效:无需 IPC,直接通过共享内存通信
适用场景:
高并发服务器(如 Web 服务器处理多个客户端请求)
计算密集型任务(如数据并行处理)
I/O 密集型任务(如同时读写多个文件或网络连接)
二、Linux 线程的底层实现机制
2.1 内核级线程与用户级线程
Linux 采用内核级线程模型,即线程的创建、调度、销毁均由内核管理,主要通过 NPTL(Native POSIX Thread Library)实现,这是 Linux 的标准线程库。
NPTL 的核心特点:
1:1 映射:每个用户线程对应一个内核轻量级进程(LWP)
高效调度:内核直接调度线程,支持多核并行执行
符合 POSIX 标准:兼容 pthread 接口,便于跨平台移植
2.2 线程控制块(task_struct)
Linux 中,线程和进程共用同一个内核数据结构task_struct,通过以下字段区分:
mm_struct *mm:指向内存描述符,线程共享同一进程的mm
struct task_struct *group_leader:指向线程组组长(即进程本身)
pid_t pid:线程的 LID(轻量级进程 ID)
pid_t tgid:线程组 ID,等于进程 ID(PID)
2.3 线程底层实现原理(详细解释)
Linux---线程_底层原理
https://blog.csdn.net/Howrun777/article/details/156840569?sharetype=blogdetail&sharerId=156840569&sharerefer=PC&sharesource=Howrun777&spm=1011.2480.3001.8118
查看进程下的所有线程
静态查看指定进程线程:优先用 ps -Lf <PID>(最常用、最直接);
实时监控线程资源:用 top -H -p <PID>(排查线程 CPU / 内存占用高的问题);
直观看线程关系:用 pstree -p <PID>(快速了解进程下的线程数量);
友好交互查看:安装 htop(新手推荐,操作更简单)。
1. ps 命令(最基础、最常用)
ps是 Linux 查看进程 / 线程的核心命令,能快速获取线程的静态快照(一次性查看,非实时)。
核心参数说明
-L:显示线程相关信息(关键!LWP列是线程内核 ID,NLWP列是进程总线程数)
-f:全格式显示(包含用户、PID、命令等)
-T:BSD 风格显示线程(SPID列是线程 ID)
-p <PID>:只显示指定进程的信息
-e:显示所有进程
查看指定进程的所有线程(最常用)
ps -Lf 1234 # 1234替换为你要查看的进程PID
AI写代码
bash
输出示例(关键列解释):
UID PID PPID LWP C NLWP STIME TTY STAT TIME CMD
root 1234 1100 1234 0 3 10:00 pts/0 Sl 0:05 ./my_app
root 1234 1100 1235 2 3 10:01 pts/0 Sl 0:12 ./my_app
root 1234 1100 1236 1 3 10:01 pts/0 Sl 0:08 ./my_app
AI写代码
bash
列名 含义
PID 线程所属的进程的 PID(同一个进程下的所有线程的 PID 都相同)实际上是内核的线程tgid, 线程的内核PID是不同的
LWP 线程的内核 ID(轻量级进程 ID,唯一标识线程)实际上是内核的进程PID
NLWP 该进程的总线程数
UID 线程所属用户
CMD 线程对应的执行命令 / 函数
Linux的线程:
用户态是从属于某一个进程的线程
内核态是一个独立的进程, 但是与某一个逻辑主进程用tgid关联
查看所有进程的所有线程
ps -eLf
BSD 风格查看指定进程的线程
ps -T -p 1234 # SPID列就是线程ID
2. top 命令(实时监控线程资源)
top是实时监控工具,能动态查看线程的 CPU、内存占用等资源使用情况,适合排查线程占用过高的问题。
进入线程视图
先输入top,进入默认的进程监控界面;
按大写 H:切换到线程视图(原本显示进程,切换后显示每个线程);
按p:按 CPU 占用率排序(找最耗 CPU 的线程);
按M:按内存占用率排序;
按q:退出 top。
直接启动线程视图
top -H # 直接显示所有线程的实时状态
只监控指定进程的线程(重点!)
top -H -p 1234 # 仅显示PID为1234的进程下的所有线程
3. pstree 命令(树形展示线程关系)
pstree以树形结构直观展示进程和线程的从属关系,一眼就能看出某个进程下有多少线程、线程的层级。
查看指定进程的线程树(显示 ID)
pstree -p 1234 # -p显示进程/线程的ID
输出示例:
nginx(1234)─┬─nginx(1235)
├─nginx(1236)
└─nginx(1237)
其中1234是主进程,1235/1236/1237是它的子线程 / 子进程。
查看所有进程的线程树(隐藏线程)
pstree -T # -T表示不显示线程,只显示进程;去掉-T则显示所有线程
4. htop 命令(更友好的交互式工具)
htop是top的增强版,界面更直观、操作更友好,自带线程视图(需要先安装)。
安装(按需执行)
# Debian/Ubuntu系统
sudo apt install htop
# CentOS/RHEL系统
sudo yum install htop
输入htop进入界面;
按F2(Setup)→ 选择Display options → 勾选Tree view(树形视图)和Show threads(显示线程);
按F4:输入进程名 / PID,过滤出目标进程的线程;
按q退出。
5. /proc 文件系统(底层查看方式)
Linux 的/proc目录是内核的 “虚拟文件系统”,所有进程 / 线程的底层信息都存在这里,适合深入排查问题。
查看指定进程的所有线程 ID
ls /proc/1234/task
# 1234是进程PID,输出的数字就是该进程下所有线程的LWP(线程ID)
查看某个线程的详细状态
cat /proc/1234/task/5678/status
# 1234=进程PID,5678=线程LWP
输出包含线程的状态(Running/Sleeping)、CPU 核心、内存占用等底层信息。
三、Linux 线程核心操作 API(pthread 库)
基础操作:pthread_create创建线程,pthread_exit终止线程,pthread_join/pthread_detach回收资源;
核心原则:可连接线程必须join(避免僵尸线程),无需返回值则用detach;
资源安全:线程取消 / 终止时,用pthread_cleanup_push/pop注册清理函数,释放锁 / 文件等资源。
线程基础操作(创建 / 终止 / 标识)
1. 线程创建:pthread_create
#include <pthread.h>
// 线程创建函数
int pthread_create(
pthread_t *thread, // 输出参数,返回创建的线程ID
const pthread_attr_t *attr, // 线程属性,NULL表示默认属性
void *(*start_routine)(void*), // 线程执行函数
void *arg // 传递给线程函数的参数
);
// 返回值:成功返回0,失败返回错误码(非0)
在当前进程中创建一个新的执行线程,新线程独立执行start_routine函数逻辑。
线程默认是「可连接状态(JOINABLE)」,需pthread_join回收资源;
线程函数签名必须严格匹配void *(*)(void*),否则编译 / 运行出错;
禁止给arg传局部变量地址(主线程退出后局部变量销毁,子线程访问会崩溃)。
参数1: pthread_t *thread:
这是一个输出型参数,用来接收函数成功创建线程后返回的「线程 ID」(POSIX 库层面的线程标识),你可以通过这个 ID 对线程做后续操作(如等待结束 pthread_join、取消线程 pthread_cancel 等)。
其底层是一个地址;
ID 的区别:
这里的 pthread_t 是「库级线程 ID」(用户态);
你之前用 ps -Lf 看到的 LWP 是「内核级线程 ID」(内核态);
可通过 pthread_self() 获取当前线程的 pthread_t,通过 syscall(SYS_gettid) 获取 LWP。
参数2: const pthread_attr_t *attr(线程属性)
用来配置新线程的属性(如线程的分离状态、栈大小、调度优先级等),决定线程的运行行为。
关键特性
可选性:传入 NULL 表示使用「默认属性」
const 修饰:const 表明 pthread_create 函数不会修改你传入的属性结构体,仅读取;
属性初始化 / 销毁:若要自定义属性,必须先调用 pthread_attr_init 初始化结构体,配置完成后传入,使用完需调用 pthread_attr_destroy 销毁(避免资源泄漏);
核心属性说明:
属性类型 作用(核心特性)
分离状态 默认:PTHREAD_CREATE_JOINABLE(可连接)—— 线程结束后需用 pthread_join 回收资源;自定义:PTHREAD_CREATE_DETACHED(分离)—— 线程结束后自动回收资源,无需 join
栈大小 默认由系统分配(通常几 MB),自定义需谨慎:过小会栈溢出(段错误),过大浪费内存;
调度策略 控制线程的 CPU 调度优先级(如 FIFO / 轮转),需 root 权限,新手极少用到;
参数3: void *(*start_routine)(void*)(线程执行函数)
指定新线程启动后要执行的核心逻辑—— 线程创建成功后,会立刻跳转到这个函数开始执行,函数执行完毕(return 或 pthread_exit),线程就会终止。
并发特性:新线程和主线程是「并发执行」的,谁先执行、执行快慢由 CPU 调度决定(无固定顺序);
参数4: void *arg(传递给线程函数的参数)
作为「通用参数」,把主线程的数据传递给新线程的执行函数(start_routine),实现主线程与子线程的数据交互。万能指针特性:void* 可以指向任意类型的数据(整数、字符串、结构体、数组等),是 C 语言中实现 “通用参数” 的常用方式;
返回值特性:
成功:返回 0;
失败:返回非 0 的错误码(不是 errno),不能用 perror() 打印,需用 strerror() 解析:
int ret = pthread_create(&tid, NULL, thread_func, NULL);
if (ret != 0) {
printf("创建线程失败:%s\n", strerror(ret)); // 解析错误码
return -1;
}
2. 线程终止:pthread_exit
void pthread_exit(void *retval);
主动终止当前线程,可选传递退出返回值(被pthread_join捕获)。
仅终止当前线程,不影响进程内其他线程;
主线程调用后,子线程仍可继续运行(区别于exit()终止整个进程);
线程函数内调用pthread_exit(ret)和return ret效果完全一致。
3. 获取当前线程 ID:pthread_self
pthread_t pthread_self(void);
返回当前执行线程的pthread_t类型 ID(库级线程 ID,非ps -Lf的 LWP)。
子线程内调用pthread_detach(pthread_self()),将自身设为分离状态。
4. 比较线程 ID:pthread_equal
int pthread_equal(pthread_t t1, pthread_t t2);
比较两个线程 ID 是否相等(因pthread_t可能是结构体,不能直接用==)。
相等返回非 0,不等返回 0;
仅比较进程内的线程 ID,跨进程无意义。
线程资源回收(等待 / 分离)
1. 等待线程终止:pthread_join
int pthread_join(pthread_t thread, void **retval);
阻塞等待指定线程终止,回收其资源(避免僵尸线程),可选获取退出返回值。
阻塞性:调用线程暂停执行,直到目标线程终止;
唯一性:一个线程只能被一次pthread_join等待,重复调用失败;
retval传NULL表示不关心返回值(新手最常用);
仅能作用于「可连接状态」线程,分离线程调用会失败。
void **retval(输出参数:获取线程退出状态): 这是一个输出型参数,用来接收目标线程终止时的「退出返回值」—— 也就是线程函数(start_routine)中 return 的指针,或调用 pthread_exit(void *retval) 时传入的指针。
返回值:成功返回 0,失败返回非 0 错误码;
2. 设置分离状态:pthread_detach
int pthread_detach(pthread_t thread);
将指定线程设置为「分离状态(PTHREAD_CREATE_DETACHED)」,分离状态的线程终止后,系统会自动回收其所有资源(栈、寄存器、内核数据结构等),无需调用 pthread_join() 等待和回收;反之,默认的「可连接状态(PTHREAD_CREATE_JOINABLE)」线程,终止后若不调用 pthread_join(),会变成「僵尸线程」,占用系统资源。
非阻塞:仅修改线程状态,调用后立即返回;
不可逆:分离状态无法转回可连接状态,也不能再join;
可在主线程调用(设子线程),也可子线程自调用pthread_detach(pthread_self())。
如果主进程在分离线程之间退出, 分离线程会直接结束;
线程属性管理(进阶配置)
线程属性通过pthread_attr_t结构体配置,核心是 “初始化→配置→使用→销毁” 流程。
1. 初始化属性:pthread_attr_init
int pthread_attr_init(pthread_attr_t *attr);
作用:初始化线程属性结构体为默认值;
必须先初始化,才能配置属性。
2. 销毁属性:pthread_attr_destroy
int pthread_attr_destroy(pthread_attr_t *attr);
作用:销毁属性结构体,释放资源;
配置完成后必须调用,避免内存泄漏。
3. 设置 / 获取分离状态
// 设置分离状态:PTHREAD_CREATE_DETACHED/JOINABLE
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
// 获取分离状态
int pthread_attr_getdetachstate(const pthread_attr_t *attr, int *detachstate);
场景:创建线程时直接指定分离状态,替代pthread_detach。
4. 设置 / 获取栈大小
// 设置线程栈大小(单位:字节)
int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);
// 获取栈大小
int pthread_attr_getstacksize(const pthread_attr_t *attr, size_t *stacksize);
注意:栈大小不能小于系统最小值(通常 16KB),过小会栈溢出(段错误)。
示例:创建分离状态的线程(通过属性)
#include <pthread.h>
pthread_attr_t attr;
pthread_attr_init(&attr); // 初始化属性
// 设置线程为分离状态
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
// 设置线程栈大小(默认栈大小通常为8MB)
size_t stack_size = 1024 * 1024; // 1MB
pthread_attr_setstacksize(&attr, stack_size);
// 设置线程调度策略(SCHED_FIFO: 实时先入先出;SCHED_RR: 实时轮转)
struct sched_param param;
param.sched_priority = 50; // 实时优先级(1-99)
pthread_attr_setschedpolicy(&attr, SCHED_FIFO);
pthread_attr_setschedparam(&attr, ¶m);
// 使用属性创建线程
pthread_create(&thread, &attr, thread_func, arg);
pthread_attr_destroy(&attr); // 销毁属性
线程取消(主动终止其他线程)
1. 请求取消线程:pthread_cancel
int pthread_cancel(pthread_t thread);
向指定线程发送 “取消请求”,线程是否响应取决于其取消状态 / 类型。
特性:
非阻塞:仅发送请求,不等待线程终止;
线程被取消后,pthread_join的retval会得到PTHREAD_CANCELED(值为(void*)-1)。
2. 设置取消状态:pthread_setcancelstate
int pthread_setcancelstate(int state, int *oldstate);
控制线程是否允许被取消:
PTHREAD_CANCEL_ENABLE(默认):允许取消;
PTHREAD_CANCEL_DISABLE:禁止取消(请求会被挂起,直到恢复允许);
oldstate:输出参数,保存旧的取消状态。
3. 设置取消类型:pthread_setcanceltype
int pthread_setcanceltype(int type, int *oldtype);
控制线程响应取消的时机:
PTHREAD_CANCEL_DEFERRED(默认):延迟取消(仅在 “取消点” 响应,如sleep/read等系统调用);
PTHREAD_CANCEL_ASYNCHRONOUS:异步取消(立即响应,极少用)。
示例:线程取消
void* thread_func(void* arg) {
// 允许取消,但延迟响应
pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);
pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED, NULL);
while (1) {
printf("线程运行中...\n");
sleep(1); // 取消点:在此处响应取消请求
}
return NULL;
}
int main() {
pthread_t tid;
pthread_create(&tid, NULL, thread_func, NULL);
sleep(3);
// 发送取消请求
pthread_cancel(tid);
void *retval;
pthread_join(tid, &retval);
if (retval == PTHREAD_CANCELED) {
printf("线程被取消\n"); // 输出此内容
}
return 0;
}
线程终止pthread_exit()和线程取消pthread_cancel()的区别?
特性 pthread_exit() pthread_cancel()
发起者 线程自己 (Self) 其他线程 (Others)
性质 主动退出 (Resignation) 被动取消 (Termination)
终止时机 立即终止 取决于取消点和取消模式 (可能延迟)
返回值 用户指定的 void* 指针 固定值 PTHREAD_CANCELED
使用场景 任务完成、发生错误需要提前结束 用户点击停止按钮、超时强制结束任务
安全性 较高,逻辑可控 较低,需小心处理资源释放和死锁
线程清理(资源安全释放)
线程终止时(正常 / 取消),自动执行注册的清理函数,避免资源泄漏。
注册清理函数:pthread_cleanup_push
触发 / 移除清理函数:pthread_cleanup_pop
void pthread_cleanup_push(void (*routine)(void*), void *arg);
void pthread_cleanup_pop(int execute);
push:注册一个清理函数(栈式,后注册先执行);
pop:若execute=1,执行清理函数并移除;若execute=0,仅移除不执行。
关键特性
成对出现:push和pop必须在同一代码块(语法上是宏,依赖{});
触发场景:线程被cancel、调用pthread_exit、pop(execute=1)时执行。
示例:线程清理(释放锁)
pthread_mutex_t mutex;
void cleanup_func(void *arg) {
// 清理函数:释放互斥锁
pthread_mutex_unlock(&mutex);
printf("清理函数:释放锁\n");
}
void* thread_func(void* arg) {
pthread_mutex_lock(&mutex);
// 注册清理函数(绑定锁地址)
pthread_cleanup_push(cleanup_func, &mutex);
printf("线程持有锁,等待被取消...\n");
sleep(5); // 取消点
// 若正常退出,pop(0)不执行清理函数
pthread_cleanup_pop(0);
pthread_mutex_unlock(&mutex);
return NULL;
}
int main() {
pthread_mutex_init(&mutex, NULL);
pthread_t tid;
pthread_create(&tid, NULL, thread_func, NULL);
sleep(2);
pthread_cancel(tid); // 取消线程,触发清理函数
pthread_join(tid, NULL);
pthread_mutex_destroy(&mutex);
return 0;
}
输出:
线程持有锁,等待被取消...
清理函数:释放锁
AI写代码
示例一:基础版 (创建、传参、回收)
这个程序会启动 3 个线程,每个线程打印自己的 ID,模拟工作 1 秒,然后退出。
文件名:simple_thread.c
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h> // 必须包含这个头文件
#include <unistd.h> // 为了使用 sleep()
// 定义一个结构体用来传参
// 因为 pthread_create 的参数只有一个 void*,所以多个参数要封装成结构体
struct ThreadData {
int thread_id;
char *message;
};
// 线程要执行的函数
// 格式必须是:void* func_name(void* arg)
void* thread_function(void *arg) {
// 1. 将 void* 强转回原本的结构体指针
struct ThreadData *data = (struct ThreadData*)arg;
printf("[线程 %d] 正在启动... 消息: %s\n", data->thread_id, data->message);
// 2. 模拟耗时工作
sleep(1);
printf("[线程 %d] 工作完成,准备退出。\n", data->thread_id);
// 3. 退出线程
pthread_exit(NULL);
}
int main() {
pthread_t threads[3]; // 存放线程句柄
struct ThreadData thread_args[3]; // 存放每个线程的参数
int rc;
int i;
printf("--- 主线程开始 ---\n");
// 1. 循环创建线程
for(i = 0; i < 3; i++) {
// 准备参数
thread_args[i].thread_id = i;
thread_args[i].message = "Hello Linux";
printf("主线程: 正在创建线程 %d\n", i);
// 核心函数:pthread_create
// 参数1: 线程句柄指针
// 参数2: 线程属性 (NULL 代表默认)
// 参数3: 线程运行的函数指针
// 参数4: 传递给函数的参数 (必须转为 void*)
rc = pthread_create(&threads[i], NULL, thread_function, (void *)&thread_args[i]);
if (rc) {
printf("错误: 无法创建线程, %d\n", rc);
exit(-1);
}
}
// 2. 等待所有线程结束 (Join)
// 如果不加这一步,主线程跑完 main 函数直接 return,所有子线程会被强制杀死
for(i = 0; i < 3; i++) {
pthread_join(threads[i], NULL);
printf("主线程: 确认线程 %d 已回收\n", i);
}
printf("--- 所有线程结束,程序退出 ---\n");
return 0;
}
编译与运行
在 Linux 下编译线程程序,必须链接 pthread 库:
# 注意 -pthread 参数
gcc simple_thread.c -o simple_thread -pthread
# 运行
./simple_thread
示例二:进阶版 (多线程抢票 - 互斥锁演示)
这个例子演示了为什么要用互斥锁 (mutex)。如果没有锁,多个线程同时卖票,票数会变成负数或者重复卖同一张票。
文件名:mutex_thread.c
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
// 全局共享资源:只剩 10 张票
int ticket_count = 10;
// 定义一把互斥锁
pthread_mutex_t lock;
void* sell_ticket(void *arg) {
char *name = (char*)arg;
while (1) {
// --- 临界区开始:加锁 ---
pthread_mutex_lock(&lock);
if (ticket_count > 0) {
// 模拟卖票的网络延迟,增加发生竞态的概率
usleep(1000 * 100);
printf("[%s] 卖出了第 %d 张票\n", name, ticket_count);
ticket_count--;
} else {
// 没票了,必须解锁后才能跳出循环
pthread_mutex_unlock(&lock);
break;
}
// --- 临界区结束:解锁 ---
pthread_mutex_unlock(&lock);
// 卖完一张歇一会儿,让其他窗口有机会卖
usleep(1000 * 100);
}
printf("[%s] 票卖完了,下班!\n", name);
return NULL;
}
int main() {
pthread_t t1, t2;
// 1. 初始化锁
pthread_mutex_init(&lock, NULL);
// 2. 创建两个“售票窗口”线程
pthread_create(&t1, NULL, sell_ticket, "窗口A");
pthread_create(&t2, NULL, sell_ticket, "窗口B");
// 3. 等待线程结束
pthread_join(t1, NULL);
pthread_join(t2, NULL);
// 4. 销毁锁
pthread_mutex_destroy(&lock);
return 0;
}
编译与运行
gcc mutex_thread.c -o mutex_thread -pthread
./mutex_thread
四、C++ 标准库线程核心操作 API(C++11+)
C++11 及后续标准提供了跨平台(Windows 和 Linux 都可以直接使用)的线程库(<thread>/<mutex>/<atomic> 等),替代了 Linux 下的 pthread 原生 API,兼具易用性和可移植性。以下我会详细讲解 C++ 标准库对应的线程 API、特性及示例,并补充 C++ 特有的核心知识点(如 RAII、原子操作、异常安全)。
关键原则
C++ 线程优先用 “函数正常返回” 终止,避免强制终止;
资源管理必须用 RAII,杜绝手动解锁 / 清理;
简单线程同步用 std::mutex,读多写少用 std::shared_mutex(C++17);
跨平台场景避免依赖 native_handle(),优先用标准库 API。
C++标准线程库-全面讲解
https://blog.csdn.net/Howrun777/article/details/157095591?spm=1001.2014.3001.5502
示例一:基础版 (创建、传参、回收)
创建线程并传递复杂参数(结构体),获取线程 ID,等待线程结束并回收资源。
#include <iostream>
#include <thread>
#include <string>
using namespace std;
// 自定义参数结构体
struct TaskData {
int id;
string msg;
};
// 线程执行函数(接收结构体参数)
void task(const TaskData& data) {
cout << "子线程 ID:" << this_thread::get_id() << endl;
cout << "接收参数:id = " << data.id << ", msg = " << data.msg << endl;
}
int main() {
// 1. 准备参数
TaskData data{100, "Hello C++ Thread"};
// 2. 创建线程(传递结构体)
thread t(task, data);
// 3. 打印线程 ID
cout << "主线程 ID:" << this_thread::get_id() << endl;
cout << "子线程对象 ID:" << t.get_id() << endl;
// 4. 等待线程结束(回收资源)
if (t.joinable()) {
t.join();
}
cout << "线程资源已回收,程序结束" << endl;
return 0;
}
编译与运行
# 编译:指定 C++11 及以上标准,链接 pthread 库
g++ -o basic_thread basic_thread.cpp -std=c++11 -pthread
# 运行
./basic_thread
输出示例
主线程 ID:140709260886856
子线程对象 ID:140709242498816
子线程 ID:140709242498816
接收参数:id = 100, msg = Hello C++ Thread
线程资源已回收,程序结束
示例二:进阶版 (多线程抢票 - 互斥锁演示)
模拟多线程抢票场景,用 std::mutex 保护共享票池,避免竞态条件。
#include <iostream>
#include <thread>
#include <mutex>
#include <vector>
using namespace std;
// 共享资源:票池
int ticket_count = 100;
// 互斥锁:保护票池
mutex ticket_mtx;
// 抢票函数
void grab_tickets(int thread_id) {
while (true) {
// RAII 加锁:自动释放
lock_guard<mutex> lock(ticket_mtx);
// 临界区:检查并修改票池
if (ticket_count <= 0) {
break; // 票已抢完
}
// 抢票
ticket_count--;
cout << "线程 " << thread_id << " 抢票成功,剩余票数:" << ticket_count << endl;
}
cout << "线程 " << thread_id << " 抢票结束" << endl;
}
int main() {
// 创建 5 个抢票线程
vector<thread> threads;
for (int i = 1; i <= 5; i++) {
threads.emplace_back(grab_tickets, i);
}
// 等待所有线程结束
for (auto& t : threads) {
if (t.joinable()) {
t.join();
}
}
cout << "所有票已抢完,最终剩余:" << ticket_count << endl;
return 0;
}
编译与运行
# 编译
g++ -o ticket_grab ticket_grab.cpp -std=c++11 -pthread
# 运行
./ticket_grab
输出示例(部分)
线程 1 抢票成功,剩余票数:99
线程 1 抢票成功,剩余票数:98
线程 2 抢票成功,剩余票数:97
线程 3 抢票成功,剩余票数:96
...
线程 5 抢票成功,剩余票数:0
线程 5 抢票结束
线程 1 抢票结束
线程 2 抢票结束
线程 3 抢票结束
线程 4 抢票结束
所有票已抢完,最终剩余:0
核心 API 对应关系(C++ vs pthread)
pthread 功能 C++ 标准库对应 API 核心差异
线程创建 std::thread 构造函数 可移动、必须 join/detach
线程等待 / 分离 std::thread::join()/detach() 更严格的状态检查
线程 ID 获取 / 比较 std::this_thread::get_id()/std::thread::id 跨平台、可打印
线程取消 原子标志位 + 循环检查 无强制取消,更安全
资源清理 RAII(std::lock_guard/ 自定义类) 异常安全、自动释放
线程属性 std::thread::native_handle() + pthread API 标准库封装简洁,进阶需原生句柄
五、线程同步(pthread库)
因为 CPU 无法直接修改内存,它必须把数据从内存复制到寄存器(私有化),修改后再写回内存。
这个“复制 -> 修改 -> 写回”的过程不是瞬间完成的。
如果不加锁,线程 A 拿着“私有副本”修改时,线程 B 根本不知道,也会去拿原始数据搞自己的“私有副本”。锁的作用,就是强行禁止这种“各自拿着副本瞎改”的行为。
同步机制:
互斥锁:排他性访问共享资源,解决竞态;
条件变量:配合互斥锁实现线程间条件通信;
自旋锁:短临界区优先,避免线程切换;
读写锁:读多写少场景提升并发;
多线程共享资源时需同步,避免竞态条件,核心同步机制包括:互斥锁、条件变量、自旋锁、读写锁。
互斥锁(Mutex,互斥量)
举例:“厕所唯一的钥匙”
场景:家里只有一个厕所,为了防止尴尬,门上挂了一把钥匙。
规则:
你想上厕所,必须先拿到钥匙。
如果钥匙挂在那,你拿走,进屋,反锁。
如果钥匙不在,说明里面有人。你不能一直站在门口敲门(浪费体力),而是回客厅沙发上睡一会儿(让出 CPU,线程挂起/阻塞)。
里面的人出来了,把钥匙挂回原处,并把你叫醒:“喂,轮到你了”。
核心特点:排他性(一次只能一个人进)。
代价:你“去睡觉”和“被叫醒”这个过程(上下文切换)是需要花点时间的。如果里面的人只待了 0.1 秒,你这一睡一醒反而不划算。
1. 核心作用
保证共享资源的排他性访问:同一时间只有一个线程能进入 “临界区”(访问共享资源的代码段),彻底解决多线程并发修改共享资源导致的竞态条件(比如多个线程同时累加一个变量导致结果错误)。
2. 关键特点
特性 说明
排他性 锁被一个线程持有后,其他线程加锁会阻塞,直到锁被释放
阻塞式 竞争失败的线程会进入内核休眠状态(放弃 CPU),直到锁可用
非忙等 休眠时不消耗 CPU 资源,适合临界区执行时间中等的场景(毫秒级)
可重入性(默认否) 普通互斥锁不支持同一线程重复加锁(会死锁),需显式创建递归互斥锁
核心原则 加锁→访问共享资源→解锁,必须成对调用,解锁仅能由持锁线程执行
3. 调用方法(核心函数 + 完整示例)
(1)核心函数
函数 作用
pthread_mutex_init 初始化互斥锁(NULL表示使用默认属性)
pthread_mutex_lock 加锁(阻塞):锁被占用则休眠,直到获取锁
pthread_mutex_trylock 尝试加锁(非阻塞):锁被占用直接返回EBUSY,不等待
pthread_mutex_unlock 解锁:仅持有锁的线程可调用,解锁后唤醒等待队列中的一个线程
pthread_mutex_destroy 销毁互斥锁,释放内核资源
(2)完整示例(保护共享变量)
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
// 共享资源
int g_count = 0;
// 定义互斥锁
pthread_mutex_t g_mutex;
// 线程函数:累加共享变量
void* add_task(void* arg) {
for (int i = 0; i < 100000; i++) {
// 加锁:进入临界区前必须加锁
pthread_mutex_lock(&g_mutex);
// 临界区:排他访问共享变量
g_count++;
// 解锁:离开临界区必须解锁
pthread_mutex_unlock(&g_mutex);
}
return NULL;
}
int main() {
pthread_t tid1, tid2;
// 初始化互斥锁(默认属性)
if (pthread_mutex_init(&g_mutex, NULL) != 0) {
perror("mutex init failed");
exit(1);
}
// 创建两个线程并发累加
pthread_create(&tid1, NULL, add_task, NULL);
pthread_create(&tid2, NULL, add_task, NULL);
// 等待线程结束
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
// 输出结果:正确值为200000(无竞态)
printf("最终count = %d\n", g_count);
// 销毁互斥锁
pthread_mutex_destroy(&g_mutex);
return 0;
}
编译运行
gcc -o mutex_demo mutex_demo.c -lpthread
./mutex_demo # 输出:最终count = 200000
二、条件变量(Condition Variable)
举例“存钱取钱”
场景:你是负责取钱的(消费者),你爸是负责存钱的(生产者)。 你们共用一张银行卡(共享资源),这张卡必须互斥访问(要加互斥锁)。
问题:如果没有条件变量,你会怎么做?
你需要取钱,但不知道卡里有没有钱。
1. 加锁(拿到银行卡)。
2. 检查余额。
如果是 0,你没办法取。你只能解锁(把卡放回去),防止你爸没法存钱。
但是你很急,于是你过了一毫秒,又加锁,检查余额,还是 0,解锁...
再过一毫秒,又加锁...
3. 结果: 你整天就在那“拿卡、看一眼、放卡、拿卡、看一眼、放卡”。
累死你(CPU 占用率飙升): 这种空转检查叫“轮询”或“忙等待”,极其浪费资源。
累死你爸(锁竞争): 你爸想存钱,结果发现你一直霸占着卡(频繁加锁),他很难插手
如果有条件变量,你会怎么做?
1. 加锁(拿到卡)。
2. 检查余额。
如果是 0,你在条件变量(休息室) 上登记一下:“有钱了叫我”,然后自动解锁,并在休息室里呼呼大睡(线程挂起,不占 CPU)。
3. 这时候你爸来了,拿卡(因为你已经解锁了),存了 1000 块。
4. 你爸对着休息室喊一声(Signal/Notify):“钱存好啦!”。
5. 你被唤醒,重新排队拿锁,拿到锁后检查余额,发现有钱了,取走。
核心特点:等待。专门解决“抢到了锁但事情还没准备好”的场景,避免了你频繁“拿卡、看一眼、放卡、拿卡、看一眼、放卡”
1. 核心作用
实现线程间的条件通信:让一个线程 “等待某个条件满足”,直到另一个线程通知它条件已满足(比如生产者等待缓冲区有空位,消费者等待缓冲区有数据)。必须配合互斥锁使用,无法单独工作。
2. 关键特点
特性 说明
依赖互斥锁 等待时会自动释放互斥锁,被唤醒后会重新获取互斥锁(避免竞态)
阻塞式 等待的线程进入内核休眠,不消耗 CPU,直到被唤醒
避免忙等 对比 “循环检查条件”(忙等),条件变量仅在条件满足时被唤醒,效率更高
虚假唤醒 可能被内核伪唤醒(无线程调用 signal/broadcast),因此必须用while循环检查条件
唤醒策略 signal唤醒一个等待线程,broadcast唤醒所有等待线程
3. 调用方法(核心函数 + 完整示例)
(1)核心函数
函数 作用
pthread_cond_init 初始化条件变量(NULL表示默认属性)
pthread_cond_wait 等待条件满足:自动解锁 + 阻塞,被唤醒后重新加锁
pthread_cond_signal 唤醒一个等待该条件的线程(随机选一个)
pthread_cond_broadcast 唤醒所有等待该条件的线程(适用于多线程等待)
pthread_cond_destroy 销毁条件变量,释放内核资源
(2)完整示例(生产者 - 消费者模型)
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
#define BUF_SIZE 3 // 缓冲区大小
int g_buf[BUF_SIZE];
int g_idx = 0; // 缓冲区当前元素个数
// 互斥锁+条件变量
pthread_mutex_t g_mutex;
pthread_cond_t g_cond_prod; // 生产者条件:缓冲区有空位
pthread_cond_t g_cond_cons; // 消费者条件:缓冲区有数据
// 生产者线程:向缓冲区放数据
void* producer(void* arg) {
for (int i = 0; i < 5; i++) {
// 加锁保护共享资源
pthread_mutex_lock(&g_mutex);
// 缓冲区满:等待消费者取数据(while避免虚假唤醒)
while (g_idx == BUF_SIZE) {
pthread_cond_wait(&g_cond_prod, &g_mutex);
}
// 放入数据
g_buf[g_idx++] = i;
printf("生产者[%ld]:放入%d,缓冲区数量=%d\n", pthread_self(), i, g_idx);
// 唤醒消费者(缓冲区有数据了)
pthread_cond_signal(&g_cond_cons);
// 解锁
pthread_mutex_unlock(&g_mutex);
sleep(1); // 模拟生产耗时
}
return NULL;
}
// 消费者线程:从缓冲区取数据
void* consumer(void* arg) {
for (int i = 0; i < 5; i++) {
pthread_mutex_lock(&g_mutex);
// 缓冲区空:等待生产者放数据
while (g_idx == 0) {
pthread_cond_wait(&g_cond_cons, &g_mutex);
}
// 取出数据
int val = g_buf[--g_idx];
printf("消费者[%ld]:取出%d,缓冲区数量=%d\n", pthread_self(), val, g_idx);
// 唤醒生产者(缓冲区有空位了)
pthread_cond_signal(&g_cond_prod);
pthread_mutex_unlock(&g_mutex);
sleep(1); // 模拟消费耗时
}
return NULL;
}
int main() {
pthread_t tid_prod, tid_cons;
// 初始化互斥锁和条件变量
pthread_mutex_init(&g_mutex, NULL);
pthread_cond_init(&g_cond_prod, NULL);
pthread_cond_init(&g_cond_cons, NULL);
// 创建线程
pthread_create(&tid_prod, NULL, producer, NULL);
pthread_create(&tid_cons, NULL, consumer, NULL);
// 等待线程结束
pthread_join(tid_prod, NULL);
pthread_join(tid_cons, NULL);
// 销毁资源
pthread_mutex_destroy(&g_mutex);
pthread_cond_destroy(&g_cond_prod);
pthread_cond_destroy(&g_cond_cons);
return 0;
}
编译运行
gcc -o cond_demo cond_demo.c -lpthread
./cond_demo
# 输出示例:
# 生产者[140692882168576]:放入0,缓冲区数量=1
# 消费者[140692873775872]:取出0,缓冲区数量=0
# 生产者[140692882168576]:放入1,缓冲区数量=1
# 消费者[140692873775872]:取出1,缓冲区数量=0
# ...
三、自旋锁(Spin Lock)
举例: “急躁的敲门人”
场景:还是上厕所,但是这次你非常急,而且你知道里面的人通常只需 1 秒钟就能解决。
规则:
你想进厕所,发现有人。
你不回客厅睡觉(不让出 CPU,不发生线程切换)。
你就在门口原地转圈圈,每隔 0.01 秒敲一次门:“好了没?好了没?好了没?”(死循环检查)。
一旦里面的人开了门,你这一秒钟都没眨眼,直接就冲进去了。
核心特点:忙等待。
优点:省去了“回客厅睡觉”和“起床”的开销(无上下文切换),反应极快。
缺点:如果里面的人便秘(持有锁时间长),你在门口转圈圈就是纯粹浪费体力(浪费 CPU 资源,耗电)。
适用:短临界区(里面的人办事极快)。
1. 核心作用
针对极短临界区(微秒级)的排他访问:竞争锁时不进入内核休眠,而是 “忙等”(循环检测锁是否释放),避免线程切换的开销(线程切换开销约 1-10 微秒)。
2. 关键特点
特性 说明
非阻塞式(忙等) 竞争失败时不休眠,持续循环检测锁状态,直到获取锁
排他性 同一时间仅一个线程持有锁
低开销 无内核态 / 用户态切换开销,适合临界区执行时间<线程切换时间的场景
高 CPU 消耗 忙等会占用 CPU 核心,若临界区过长,会导致其他线程无法执行(CPU 浪费)
适用场景 临界区极短(如仅修改一个变量)、多核 CPU(避免单核忙等占满资源)
3. 调用方法(核心函数 + 完整示例)
(1)核心函数
函数 作用
pthread_spin_init 初始化自旋锁(第二个参数为PTHREAD_PROCESS_PRIVATE表示进程内私有)
pthread_spin_lock 加锁(忙等):锁被占用则循环检测,直到获取
pthread_spin_trylock 尝试加锁(非阻塞):锁被占用返回EBUSY,不等待
pthread_spin_unlock 解锁:释放锁,唤醒忙等的线程
pthread_spin_destroy 销毁自旋锁,释放资源
(2)完整示例(短临界区加锁)
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
long long g_counter = 0;
// 定义自旋锁
pthread_spinlock_t g_spinlock;
// 线程函数:累加计数器(临界区极短)
void* spin_task(void* arg) {
for (int i = 0; i < 100000000; i++) {
// 加自旋锁(忙等,无切换开销)
pthread_spin_lock(&g_spinlock);
// 临界区:仅一行代码(极短)
g_counter++;
// 解锁
pthread_spin_unlock(&g_spinlock);
}
return NULL;
}
int main() {
pthread_t tid1, tid2;
// 初始化自旋锁(进程内私有)
if (pthread_spin_init(&g_spinlock, PTHREAD_PROCESS_PRIVATE) != 0) {
perror("spin init failed");
exit(1);
}
// 创建两个线程
pthread_create(&tid1, NULL, spin_task, NULL);
pthread_create(&tid2, NULL, spin_task, NULL);
// 等待线程结束
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
// 输出结果:200000000
printf("最终counter = %lld\n", g_counter);
// 销毁自旋锁
pthread_spin_destroy(&g_spinlock);
return 0;
}
编译运行
gcc -o spin_demo spin_demo.c -lpthread
./spin_demo # 输出:最终counter = 200000000
四、读写锁(RW Lock,读写互斥锁)
举例: “小区的公告栏”
场景:小区门口有个黑板写通知。
规则:
读(看黑板):如果只是看,多少人同时看都行。张三在看,李四也可以凑过来一起看。大家都能同时读(并发读)。
写(改黑板):物业大爷要来擦掉旧通知写新的了。这时候,所有人必须停止读。大爷没写完字之前,不能有人看(不然会读到残缺的信息),也不能有别人写。
核心特点:读共享,写独占。
适用:读多写少。比如配置信息,一天改一次,但一秒钟被查询一万次。如果用互斥锁(厕所钥匙),一个人看的时候别人都得排队,效率太低;用读写锁,大家可以一起看。
1. 核心作用
针对读多写少的场景优化并发:允许多个线程同时加 “读锁”(共享访问),但写锁是排他的(加写锁时,所有读锁 / 写锁都需等待),大幅提升读操作的并发度。
2. 关键特点
特性 说明
读写分离 读锁共享(多线程同时读),写锁排他(仅一个线程写)
阻塞式 读锁竞争写锁:写锁阻塞所有读 / 写;读锁之间无阻塞
写优先级(默认) 写锁请求会优先于读锁(避免写操作饥饿)
适用场景 读操作远多于写操作(如配置文件读取、缓存查询、日志查看)
核心原则 读共享、写排他;读写锁解锁统一调用pthread_rwlock_unlock
3. 调用方法(核心函数 + 完整示例)
(1)核心函数
函数 作用
pthread_rwlock_init 初始化读写锁(NULL表示默认属性)
pthread_rwlock_rdlock 加读锁:共享访问,多个线程可同时持有
pthread_rwlock_wrlock 加写锁:排他访问,阻塞所有读 / 写线程
pthread_rwlock_unlock 解锁:无论读锁 / 写锁,统一用此函数释放
pthread_rwlock_destroy 销毁读写锁,释放内核资源
(2)完整示例(读多写少场景)
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
// 共享资源:模拟配置数据
int g_config = 100;
// 定义读写锁
pthread_rwlock_t g_rwlock;
// 读线程:读取配置(大量)
void* read_task(void* arg) {
long tid = (long)arg;
for (int i = 0; i < 3; i++) {
// 加读锁(共享)
pthread_rwlock_rdlock(&g_rwlock);
printf("读线程[%ld]:读取配置 = %d\n", tid, g_config);
sleep(1); // 模拟读耗时
// 解锁
pthread_rwlock_unlock(&g_rwlock);
sleep(1);
}
return NULL;
}
// 写线程:修改配置(少量)
void* write_task(void* arg) {
long tid = (long)arg;
for (int i = 0; i < 2; i++) {
// 加写锁(排他)
pthread_rwlock_wrlock(&g_rwlock);
g_config += 10;
printf("=== 写线程[%ld]:修改配置为 %d ===\n", tid, g_config);
sleep(1); // 模拟写耗时
// 解锁
pthread_rwlock_unlock(&g_rwlock);
sleep(2);
}
return NULL;
}
int main() {
pthread_t tid_read1, tid_read2, tid_read3, tid_write;
// 初始化读写锁
if (pthread_rwlock_init(&g_rwlock, NULL) != 0) {
perror("rwlock init failed");
exit(1);
}
// 创建3个读线程 + 1个写线程
pthread_create(&tid_read1, NULL, read_task, (void*)1);
pthread_create(&tid_read2, NULL, read_task, (void*)2);
pthread_create(&tid_read3, NULL, read_task, (void*)3);
pthread_create(&tid_write, NULL, write_task, (void*)100);
// 等待所有线程结束
pthread_join(tid_read1, NULL);
pthread_join(tid_read2, NULL);
pthread_join(tid_read3, NULL);
pthread_join(tid_write, NULL);
// 销毁读写锁
pthread_rwlock_destroy(&g_rwlock);
return 0;
}
编译运行
gcc -o rwlock_demo rwlock_demo.c -lpthread
./rwlock_demo
# 输出示例(3个读线程同时读,写线程独占写):
# 读线程[1]:读取配置 = 100
# 读线程[2]:读取配置 = 100
# 读线程[3]:读取配置 = 100
# === 写线程[100]:修改配置为 110 ===
# 读线程[1]:读取配置 = 110
# 读线程[2]:读取配置 = 110
# 读线程[3]:读取配置 = 110
# === 写线程[100]:修改配置为 120 ===
总结(核心选型建议)
同步机制 核心优势 核心缺点 最佳适用场景
互斥锁 通用、不耗 CPU、适用范围广 有线程切换开销 临界区执行时间中等(毫秒级)
条件变量 精准等待条件、避免忙等 依赖互斥锁、逻辑稍复杂 线程间条件通信(生产者 - 消费者)
自旋锁 无线程切换开销、速度快 忙等消耗 CPU 临界区极短(微秒级)、多核 CPU
读写锁 读多写少场景提升并发 写操作阻塞所有读 / 写 读操作远多于写操作(配置 / 缓存)
核心选型原则:
优先用互斥锁(通用),不确定场景时选它;
需要 “等待条件” 用条件变量(必须配互斥锁);
临界区极短(仅 1-2 行代码)用自旋锁;
读多写少用读写锁,提升读并发。
线程局部存储(TLS)(C++11)
线程局部存储允许每个线程拥有独立的变量副本,避免线程间数据干扰。
实现方式:
.现代 C++ (C++11 及以后) —— 推荐
使用 thread_local 关键字:
#include <iostream>
#include <thread>
// 定义 TLS 变量
thread_local int my_counter = 0;
void func(const char* name) {
my_counter++; // 每个线程都在加自己的那个 counter
std::cout << name << ": " << my_counter << std::endl;
}
int main() {
std::thread t1(func, "Thread A");
std::thread t2(func, "Thread B");
t1.join();
t2.join();
// 输出:
// Thread A: 1
// Thread B: 1
// 互不干扰,不用加锁
}
C 语言 / GCC 扩展
使用__thread关键字(GCC 扩展):
#include <stdio.h>
#include <pthread.h>
// 变量thread_local_var成为每个线程独立的变量
__thread int thread_local_var = 0;
void* thread_func(void* arg) {
thread_local_var = *(int*)arg;
printf("线程ID: %lu, 局部变量值: %d\n", (unsigned long)pthread_self(), thread_local_var);
return NULL;
}
int main() {
pthread_t t1, t2;
int arg1 = 10, arg2 = 20;
pthread_create(&t1, NULL, thread_func, &arg1);
pthread_create(&t2, NULL, thread_func, &arg2);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
return 0;
}
使用 pthread_key_create(POSIX 标准接口):
如果编译器不支持关键字,或者需要动态分配 TLS,可以使用 pthread_key_create, pthread_setspecific, pthread_getspecific。这就比较繁琐,类似于手动在一个全局哈希表里用“线程ID”当 Key 存取数据。
#include <stdio.h>
#include <pthread.h>
pthread_key_t key; // 线程局部存储键
// 线程退出时销毁局部存储数据
void destructor(void* data) {
free(data);
}
void* thread_func(void* arg) {
int* value = malloc(sizeof(int));
*value = *(int*)arg;
pthread_setspecific(key, value); // 设置线程局部存储
printf("线程ID: %lu, 局部变量值: %d\n", (unsigned long)pthread_self(), *(int*)pthread_getspecific(key));
return NULL;
}
int main() {
pthread_key_create(&key, destructor); // 创建键并设置析构函数
pthread_t t1, t2;
int arg1 = 10, arg2 = 20;
pthread_create(&t1, NULL, thread_func, &arg1);
pthread_create(&t2, NULL, thread_func, &arg2);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
pthread_key_delete(key); // 删除键
return 0;
}
线程安全与可重入函数
线程安全:函数在多线程环境下调用时,无论线程如何调度,都能保证结果正确。
可重入函数:函数可以被多个线程同时调用,且不会因为自身状态导致错误。
常见线程安全函数:
标准库函数:printf(内部加锁)、malloc(线程安全实现)
自定义函数:通过互斥锁保护共享资源的函数
注意事项:
避免在信号处理函数中调用不可重入函数(如printf、malloc)
全局变量和静态变量是线程安全的主要隐患,需通过同步机制保护
六、Linux 线程常见问题与解决方案
6.1 死锁(Deadlock)
定义:多个线程互相等待对方释放资源,导致所有线程都无法继续执行
产生条件(同时满足):
互斥条件:资源不能被多个线程同时占用
请求与保持条件:线程持有资源的同时请求其他线程的资源
不剥夺条件:资源只能被持有线程主动释放
循环等待条件:线程形成环形的资源等待链
解决方案:
按固定顺序获取锁
使用超时机制(pthread_mutex_timedlock)
避免嵌套锁
使用死锁检测算法
用生活场景理解死锁
最经典的类比是 **“两个人抢东西”**:假设你和朋友一起吃饭,桌上只有一双筷子和一把勺子
你先拿起了筷子,准备夹菜,但发现需要勺子喝汤;
朋友同时拿起了勺子,准备喝汤,但发现需要筷子夹菜;
你等着朋友放下勺子,朋友等着你放下筷子,你们都不肯先放手,结果谁都吃不了饭,一直僵持下去 —— 这就是 “死锁”。
死锁的 4 个 “必备条件”(缺一不可)
用上面的例子对应,就能明白死锁必须同时满足这 4 个条件:
互斥条件:资源只能被一个人占用(筷子 / 勺子 / 同一时间只能一个人用)
请求与保持条件:拿着自己的资源,还想要别人的(你拿着筷子还想要勺子,朋友拿着勺子还想要筷子)
不剥夺条件:别人不能强行抢走你的资源(朋友不能硬抢你的筷子,你不能抢朋友的勺子)
循环等待条件:形成了 “你等我、我等你” 的闭环(你等朋友的勺子,朋友等你的筷子)
怎么解决死锁?
只要打破上面任意一个条件就行,比如:
打破循环等待:约定好所有人都按 “先拿筷子、再拿勺子” 的顺序拿资源,就不会出现互相等的情况;
打破请求与保持:要么一次性把需要的资源都拿到手,要么拿到一个资源用完就立刻释放,再拿下一个;
打破不剥夺:设置超时时间,等不到资源就主动放弃自己手里的资源,过一会儿再重试
6.2 线程泄漏
定义:线程创建后未被正确回收,导致系统资源(如内核栈、task_struct)被持续占用。
解决方案:
对非分离线程,必须调用pthread_join回收
对不需要等待的线程,设置为分离状态(pthread_detach)
避免在循环中创建线程而不回收
6.3 虚假唤醒(Spurious Wakeup)
定义:条件变量在没有被显式唤醒的情况下,线程从pthread_cond_wait返回。
解决方案:
使用while循环代替if判断条件,确保被唤醒后条件确实满足:
// 错误写法(可能导致虚假唤醒)
if (条件不满足) {
pthread_cond_wait(&cond, &mutex);
}
// 正确写法
while (条件不满足) {
pthread_cond_wait(&cond, &mutex);
}
————————————————
版权声明:本文为CSDN博主「Howrun777」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/Howrun777/article/details/156518699
云服务器爆款直降90%
新客首单¥68起 | 人人可享99元套餐,续费同价 | u2a指定配置低至2.5折1年,立即选购享更多福利!