本节重点:
- 了解线程概念,理解线程与进程区别与联系。
- 学会线程控制,线程创建,线程终止,线程等待。
- 了解线程分离与线程安全概念。
- 学会线程同步。
- 学会使用互斥量,条件变量,posix信号量,以及读写锁。
- 理解基于读写锁的读者写者问题
1. Linux线程概念
什么是线程
在一个程序里的一个执行路线就叫做线程(thread)。更准确的定义是:线程是"一个进程内部的控制序列" 一切进程至少都有一个执行线程
线程在进程内部运行,本质是在进程地址空间内运行
在Linux系统中,在CPU眼中,看到的PCB都要比传统的进程更加轻量化
透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流

- 进程与线程的关系 💡
- Linux 线程 = 轻量级进程 (LWP) :在 Linux 内核看来,线程和进程的本质都是
task_struct,只是线程会共享父进程的地址空间(mm_struct),而进程拥有独立的地址空间。 task_struct:这是 Linux 内核描述 "执行流" 的核心数据结构,也就是我们常说的 进程控制块 (PCB)。它包含了进程 / 线程的所有元信息,如 PID、状态、寄存器、内存指针等。mm_struct:代表进程的虚拟内存空间 。每个进程有一个独立的mm_struct,而同一进程内的所有线程共享同一个mm_struct。
- 虚拟地址空间布局 🗺️
每个进程的虚拟地址空间(mm_struct)被划分为不同的区域,从高地址到低地址依次是:
- 内核区:供内核代码和数据使用,对用户态进程不可见。
- 栈区 (Stack):用于函数调用、局部变量和返回地址,由高地址向低地址增长。
- 共享区 (Memory Mapping Segment):用于加载动态链接库、文件映射等。
- 堆区 (Heap) :用于动态内存分配(如
malloc),由低地址向高地址增长。 - 未初始化数据段 (BSS):存放未初始化的全局变量和静态变量,程序启动时会被清零。
- 已初始化数据段 (Data):存放已初始化的全局变量和静态变量。
- 代码段 (Text):存放程序的可执行机器指令,通常是只读的。
- 虚拟地址到物理地址的映射 🔄
- 页表机制 :虚拟地址并非直接指向物理内存。CPU 通过页表 (Page Table) 完成地址翻译。以一级页表为例,虚拟地址被拆分为 "页目录索引 + 页表索引 + 页内偏移",通过多级查找最终得到物理页框号。
- 缺页中断 :如果访问的虚拟页对应的物理页不在内存中,CPU 会触发缺页中断 (Page Fault),内核会负责分配物理内存、从磁盘加载数据,并更新页表,建立新的映射关系。
- 页表项 (PTE) 信息 :每个页表项不仅包含物理地址,还包含了重要的元数据:
- 是否在内存中 (Present):标记该页是否已加载到物理内存。
- 权限 (RWX):标记该页是可读、可写还是可执行。
- 权限级别 (U/K):标记该页是用户态 (User) 还是内核态 (Kernel) 可访问。
- 执行流共享地址空间 🤝
图中明确指出 "执行流共享地址空间",这正是线程的核心特征:
- 同一进程下的多个线程(
task_struct)共享同一个mm_struct,因此它们看到的代码、数据、堆、共享库都是一样的。 - 线程之间的切换成本远低于进程,因为不需要切换地址空间,只需切换上下文(寄存器等)。
1.1线程的优点
创建一个新线程的代价要比创建一个新进程小得多
与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
线程占用的资源要比进程少很多
能充分利用多处理器的可并行数量
在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。
1.2****线程的缺点
性能损失
一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。
健壮性降低
编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。
缺乏访问控制
进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。
编程难度提高
编写与调试一个多线程程序比单线程程序困难得多
1.3线程异常
单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃
线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出
1.4线程用途
合理的使用多线程,能提高CPU密集型程序的执行效率
合理的使用多线程,能提高IO密集型程序的用户体验(如生活中我们一边写代码一边下载开发工具,就是多线程运行的一种表现)
2. Linux进程VS线程
2.1进程和线程
进程是资源分配的基本单位
线程是调度的基本单位
线程共享进程数据,但也拥有自己的一部分数据:
线程ID
一组寄存器
栈
errno
信号屏蔽字
调度优先级
进程的多个线程共享 同一地址空间,因此Text Segment、Data Segment都是共享的,如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境:
文件描述符表
每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)
当前工作目录
用户id和组id
进程和线程的关系如下图:

1. 单线程进程(左上)
- 结构:一个大框(进程)里只有一条曲线(线程)。
- 含义:这是最传统的模型,一个进程内部只包含一个执行流。整个程序的代码从头到尾顺序执行。
- 特点:结构简单,没有并发问题,但无法利用多核 CPU,执行效率受限。
2. 单进程多线程(右上)
- 结构:一个大框(进程)里包含多条曲线(多个线程)。
- 含义:一个进程内部创建了多个并发执行的线程,它们共享同一个地址空间和资源(如文件句柄、全局变量)。
- 特点:线程间切换开销小,通信方便(直接读写共享内存),但需要精心设计同步机制(如互斥锁)来避免数据竞争。
3. 多个单线程进程(左下)
- 结构:多个独立的大框(进程),每个框里只有一条曲线(线程)。
- 含义:系统中同时运行着多个独立的程序,每个程序都是单线程的。
- 特点:进程间有严格的隔离,一个进程崩溃不会影响其他进程,但进程间通信(IPC)成本高,且每个进程都有独立的地址空间,内存开销大。
4. 多个多线程进程(右下)
- 结构:多个独立的大框(进程),每个框里又包含多条曲线(多个线程)。
- 含义:这是现代操作系统和应用程序的主流模型。系统中运行着多个多线程程序,每个程序内部又有多个并发线程。
- 特点:结合了多进程的隔离性和多线程的高效性,既能充分利用多核 CPU,又能保证系统的稳定性和安全性。
3. Linux线程控制
3.1POSIX线程库
与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以"pthread_"打头的
要使用这些函数库,要通过引入头文<pthread.h>
链接这些线程函数库时要使用编译器命令的"-lpthread"选项
3.2创建线程
cpp
功能:创建一个新的线程
原型
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *
(*start_routine)(void*), void *arg);
参数
thread:返回线程ID
attr:设置线程的属性,attr为NULL表示使用默认属性
start_routine:是个函数地址,线程启动后要执行的函数
arg:传给线程启动函数的参数
返回值:成功返回0;失败返回错误码
错误检查:
传统的一些函数是,成功返回0,失败返回-1,并且对全局变量errno赋值以指示错误。
pthreads函数出错时不会设置全局变量errno(而大部分其他POSIX函数会这样做)。而是将错误代码通过返回值返回
pthreads同样也提供了线程内的errno变量,以支持其它使用errno的代码。对于pthreads函数的错误,建议通过返回值业判定,因为读取返回值要比读取线程内的errno变量的开销更小
cpp
#include <unistd.h> // 提供 sleep() 函数(系统调用,用于线程休眠)
#include <stdlib.h> // 提供 exit()、EXIT_FAILURE 等函数/宏
#include <stdio.h> // 提供 printf()、fprintf() 等输入输出函数
#include <string.h> // 提供 strerror() 函数(将错误码转为错误信息)
#include <pthread.h> // 提供 pthread 线程库的所有接口(创建、管理线程)
void *rout(void *arg) { // pthread 线程函数固定格式:返回 void*,参数 void*
int i; // 无实际作用的局部变量(代码里未使用)
for( ; ; ) { // 无限循环(等价于 while(1))
printf("I'am thread 1\n"); // 子线程打印标识
sleep(1); // 线程休眠 1 秒,让出 CPU 资源
}
}
int main( void )
{
pthread_t tid; // 定义 pthread_t 类型变量,用于存储新线程的 ID
int ret; // 存储 pthread_create 的返回值(错误码)
// 创建新线程
if ( (ret=pthread_create(&tid, NULL, rout, NULL)) != 0 ) {
// 创建失败:打印错误信息(strerror 把错误码转为可读字符串)
fprintf(stderr, "pthread_create : %s\n", strerror(ret));
exit(EXIT_FAILURE); // 退出程序,返回失败状态
}
int i; // 无实际作用的局部变量
for(; ; ) { // 主线程无限循环
printf("I'am main thread\n"); // 主线程打印标识
sleep(1); // 休眠 1 秒
}
}
这里再说明一个问题当主线程退出时,其他线程会不会退出?
答案是不一定。
因为在 Linux 中:
进程是资源分配的基本单位,线程是调度执行的基本单位。所有线程共享进程的资源,当进程的最后一个线程(通常是主线程)退出时,整个进程会被销毁,进程内的所有线程都会被强制终止。
但是如果主线程不是最后一个退出的线程呢?
场景一:主线程退出,但进程未退出(其他线程仍运行)
如果主线程只是 "退出执行"(比如调用 pthread_exit(NULL)),但进程还存在(有其他线程在运行),那么子线程会继续执行.
cpp
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <pthread.h>
void *rout(void *arg) {
for( ; ; ) {
printf("I'am thread 1\n");
sleep(1);
}
}
int main( void )
{
pthread_t tid;
int ret;
if ( (ret=pthread_create(&tid, NULL, rout, NULL)) != 0 ) {
fprintf(stderr, "pthread_create : %s\n", strerror(ret));
exit(EXIT_FAILURE);
}
// 主线程执行 3 秒后退出(调用 pthread_exit,仅退出主线程,进程不销毁)
sleep(3);
printf("主线程退出,但进程还在\n");
pthread_exit(NULL); // 关键:主线程退出,进程保留
// 下面的代码不会执行
printf("不会打印\n");
return 0;
}
运行结果:
- 前 3 秒:主线程和子线程交替打印;
- 3 秒后:主线程退出,子线程继续无限打印(进程仍存活)
场景 2:主线程退出,导致进程退出(其他线程被强制终止)
如果主线程通过 return、exit() 或直接结束执行退出,会触发进程退出,此时所有子线程会被强制终止。
cpp
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <pthread.h>
void *rout(void *arg) {
for( ; ; ) {
printf("I'am thread 1\n");
sleep(1);
}
}
int main( void )
{
pthread_t tid;
int ret;
if ( (ret=pthread_create(&tid, NULL, rout, NULL)) != 0 ) {
fprintf(stderr, "pthread_create : %s\n", strerror(ret));
exit(EXIT_FAILURE);
}
// 主线程执行 3 秒后退出(调用 exit,直接销毁进程)
sleep(3);
printf("主线程退出,进程销毁\n");
exit(EXIT_SUCCESS); // 关键:主线程退出 → 进程退出 → 所有线程终止
return 0;
}
运行结果:
- 前 3 秒:主线程和子线程交替打印;
- 3 秒后:打印 "主线程退出,进程销毁",程序直接终止,子线程也停止运行。
关键区别:pthread_exit vs exit/return
| 操作 | 主线程状态 | 进程状态 | 子线程状态 |
|---|---|---|---|
pthread_exit(NULL) |
退出 | 存活 | 继续运行 |
exit()/return |
退出 | 销毁 | 强制终止 |
3.3线程ID及进程地址空间布局
pthread_ create函数会产生一个线程ID,存放在第一个参数指向的地址中。该线程ID和前面说的线程ID 不是一回事。
前面讲的线程ID属于进程调度的范畴。因为线程是轻量级进程,是操作系统调度器的最小单位,所以需要 一个数值来唯一表示该线程。
pthread_ create函数第一个参数指向一个虚拟内存单元,该内存单元的地址即为新创建线程的线程ID,属于NPTL线程库的范畴。线程库的后续操作,就是根据该线程ID来操作线程的。
线程库NPTL提供了pthread_ self函数,可以获得线程自身的ID.
详细点说就是:
线程库的线程ID与内核的LWP ID不是一样的,在内核层线程是当作轻量级进程的,而在用户层也就是线程库中,线程就是一个线程不会当做进程。
| 维度 | pthread 库的线程 ID(pthread_t) | 内核的 LWP ID(轻量级进程 ID) |
|---|---|---|
| 归属范畴 | NPTL(Native POSIX Thread Library)线程库 | Linux 内核调度器 |
| 数据类型 | 通常是结构体指针 / 无符号长整型(依赖实现) | 整型(和进程 PID 同类型,pid_t) |
| 作用域 | 进程内唯一(不同进程的 pthread_t 可能重复) | 系统内全局唯一 |
| 核心作用 | 线程库层面操作线程(如 pthread_join、pthread_cancel) | 内核调度线程(CPU 分配、优先级管理) |
| 获取方式 | pthread_self() |
gettid() 或 ps -L 命令 |
结合 pthread_create 理解两个 ID
cpp
int pthread_create(pthread_t *tid, // 存储pthread库的线程ID(pthread_t)
const pthread_attr_t *attr,
void *(*start_routine) (void *),
void *arg);
1. pthread 库的线程 ID(pthread_t)
- 这是线程库为了管理线程而生成的 ID,本质是线程库内部的一个标识(在 Linux 下,pthread_t 实际是指向线程描述符的指针)。
- 这个 ID 只在当前进程内有效,线程库的所有函数(
pthread_join/pthread_detach/pthread_cancel)都依赖这个 ID 来操作线程。 - 用
pthread_self()可以获取当前线程的 pthread_t ID:
cpp
pthread_t tid = pthread_self(); // 获取当前线程的pthread库ID
2. 内核的 LWP ID(轻量级进程 ID)
- Linux 内核没有真正的 "线程" 概念,线程本质是共享地址空间的轻量级进程(LWP),内核给每个 LWP 分配一个全局唯一的 ID(和进程 PID 一样是 pid_t 类型)。
- 这个 ID 是内核调度的依据,
top -H、ps -L <PID>命令看到的线程 ID 就是 LWP ID:
cpp
ps -L 1234 # 查看进程1234下所有线程的LWP ID
top -H # 按线程维度显示CPU占用(显示的是LWP ID)
代码中获取 LWP ID 需要调用系统调用 gettid()(注意:glibc 未封装,需手动调用):
cpp
#include <sys/syscall.h>
#include <unistd.h>
pid_t lwp_id = syscall(SYS_gettid); // 获取当前线程的内核LWP ID
用代码直观区分两个 ID
cpp
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <pthread.h>
#include <sys/syscall.h>
// 定义获取LWP ID的宏
#define gettid() syscall(SYS_gettid)
void *rout(void *arg) {
// 子线程:打印pthread ID和LWP ID
printf("子线程 - pthread ID: %lu, LWP ID: %d\n",
(unsigned long)pthread_self(), gettid());
for( ; ; ) {
sleep(1);
}
}
int main( void )
{
pthread_t tid; // 存储pthread库的线程ID
int ret;
if ( (ret=pthread_create(&tid, NULL, rout, NULL)) != 0 ) {
fprintf(stderr, "pthread_create : %s\n", strerror(ret));
exit(EXIT_FAILURE);
}
// 主线程:打印自身的pthread ID、LWP ID,以及子线程的pthread ID
printf("主线程 - pthread ID: %lu, LWP ID: %d\n",
(unsigned long)pthread_self(), gettid());
printf("子线程的pthread ID(主线程视角): %lu\n", (unsigned long)tid);
// 等待子线程(避免主线程提前退出)
pthread_join(tid, NULL);
return 0;
}
cpp
主线程 - pthread ID: 140709267195712, LWP ID: 12345
子线程的pthread ID(主线程视角): 140709258802944
子线程 - pthread ID: 140709258802944, LWP ID: 12346
那么pthread_t 到底是什么类型呢?取决于实现。对于Linux目前实现的NPTL实现而言,pthread_t类型的线程ID,本质就是一个进程地址空间上的一个地址。

3.4线程终止
如果需要只终止某个线程而不终止整个进程,可以有三种方法:
- 从线程函数return。这种方法对主线程不适用,从main函数return相当于调用exit。
- 线程可以调用pthread_ exit终止自己。
- 一个线程可以调用pthread_ cancel终止同一进程中的另一个线程。
pthread_exit函数
cpp
功能:线程终止
原型
void pthread_exit(void *value_ptr);
参数
value_ptr:value_ptr不要指向一个局部变量。
返回值:无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)
需要注意,pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。
pthread_cancel函数
cpp
功能:取消一个执行中的线程
原型
int pthread_cancel(pthread_t thread);
参数
thread:线程ID
返回值:成功返回0;失败返回错误码
3.5线程等待
为什么需要线程等待?
已经退出的线程,其空间没有被释放,仍然在进程的地址空间内。
创建新的线程不会复用刚才退出线程的地址空间。
详细点说就是:
线程等待(核心函数 pthread_join)的本质是回收已退出线程的资源,避免内存泄漏和资源浪费------ 如果不等待,已退出线程会变成 "僵尸线程",占用进程地址空间且无法被复用,最终拖垮整个进程。
cpp
功能:等待线程结束
原型
int pthread_join(pthread_t thread, void **value_ptr);
参数
thread:线程ID
value_ptr:它指向一个指针,后者指向线程的返回值
返回值:成功返回0;失败返回错误码
调用该函数的线程将挂起等待,直到id为thread的线程终止。thread线程以不同的方法终止,通过pthread_join得到的
终止状态是不同的,总结如下:
-
如果thread线程通过return返回,value_ ptr所指向的单元里存放的是thread线程函数的返回值。
-
如果thread线程被别的线程调用pthread_ cancel异常终掉,value_ ptr所指向的单元里存放的是常数PTHREAD_ CANCELED。
-
如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传给pthread_exit的参数。
-
如果对thread线程的终止状态不感兴趣,可以传NULL给value_ ptr参数。

-
第一组:线程正常返回
-
主线程 :调用
pthread_create创建子线程,子线程开始执行。 -
子线程 :执行任务后通过
return语句返回(等价于pthread_exit((void*)retval))。 -
主线程 :调用
pthread_join阻塞等待,直到子线程退出,回收其资源并获取返回值。 -
第二组:线程主动退出
-
主线程 :再次调用
pthread_create创建新的子线程。 -
子线程 :执行过程中主动调用
pthread_exit退出,可携带退出状态。 -
主线程 :再次调用
pthread_join等待并回收该子线程的资源。 -
第三组:线程被取消
-
主线程 :第三次调用
pthread_create创建子线程,子线程进入循环执行。 -
主线程 :调用
pthread_cancel向子线程发送取消请求。 -
子线程:接收到取消信号后终止执行。
-
主线程 :最后调用
pthread_join等待并回收被取消线程的资源。 -
主线程 :所有子线程处理完毕后,主线程执行
exit退出,整个进程结束。
cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
// 线程1:通过return返回退出状态(动态分配内存存储返回值)
void *thread1( void *arg )
{
printf("thread 1 returning ... \n");
// 动态分配int大小的内存,存储线程退出码
// 注意:必须用动态内存,栈内存会随线程退出释放,主线程获取时会野指针
int *p = (int*)malloc(sizeof(int));
*p = 1; // 线程1的退出码设为1
// return等价于pthread_exit,返回值会被pthread_join捕获
return (void*)p;
}
// 线程2:通过pthread_exit主动退出,携带退出状态
void *thread2( void *arg )
{
printf("thread 2 exiting ...\n");
// 同样动态分配内存存储退出码
int *p = (int*)malloc(sizeof(int));
*p = 2; // 线程2的退出码设为2
// 主动调用pthread_exit退出线程,效果和return一致,但更灵活(可在函数任意位置调用)
pthread_exit((void*)p);
}
// 线程3:无限循环运行,只能通过外部pthread_cancel取消
void *thread3( void *arg )
{
while ( 1 ){ // 无限循环,无退出条件
printf("thread 3 is running ...\n");
sleep(1); // 每秒打印一次,模拟持续运行的任务
}
return NULL; // 理论上不会执行到这里
}
int main( void )
{
pthread_t tid; // 存储pthread库的线程ID(用户层ID)
void *ret; // 存储线程退出时的返回值(pthread_join的第二个参数)
// ========== 测试线程1:通过return返回 ==========
// 创建线程1,执行thread1函数,无参数传递
pthread_create(&tid, NULL, thread1, NULL);
// 阻塞等待线程1退出,回收资源,并获取退出返回值到ret中
pthread_join(tid, &ret);
// 打印线程ID(十六进制)和退出码(注意ret是void*,需强转为int*再取值)
printf("thread return, thread id %X, return code:%d\n", tid, *(int*)ret);
free(ret); // 释放线程1中malloc的内存,避免内存泄漏
// ========== 测试线程2:通过pthread_exit退出 ==========
// 创建线程2,执行thread2函数
pthread_create(&tid, NULL, thread2, NULL);
// 阻塞等待线程2退出,回收资源
pthread_join(tid, &ret);
// 打印线程2的ID和退出码
printf("thread return, thread id %X, return code:%d\n", tid, *(int*)ret);
free(ret); // 释放线程2中malloc的内存
// ========== 测试线程3:通过pthread_cancel取消 ==========
// 创建线程3,执行thread3函数(无限循环)
pthread_create(&tid, NULL, thread3, NULL);
sleep(3); // 主线程休眠3秒,让线程3运行3次打印
// 向线程3发送取消请求,线程3会在合适的取消点(如sleep)终止
pthread_cancel(tid);
// 等待线程3退出,回收资源
pthread_join(tid, &ret);
// 判断线程是否被取消:被取消的线程返回值为PTHREAD_CANCELED(宏定义,值为(void*)-1)
if ( ret == PTHREAD_CANCELED )
printf("thread return, thread id %X, return code:PTHREAD_CANCELED\n", tid);
else
printf("thread return, thread id %X, return code:NULL\n", tid);
return 0;
}
cpp
运行结果:
[root@localhost linux]# ./a.out
thread 1 returning ...
thread return, thread id 5AA79700, return code:1
thread 2 exiting ...
thread return, thread id 5AA79700, return code:2
thread 3 is running ...
thread 3 is running ...
thread 3 is running ...
thread return, thread id 5AA79700, return code:PTHREAD_CANCELED
4. 分离线程
默认情况下,新创建的线程是joinable(可连接,需要调用join或者detach)的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。
如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。
cpp
int pthread_detach(pthread_t thread);
可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离:
cpp
pthread_detach(pthread_self());
joinable和分离是冲突的,一个线程不能既是joinable又是分离的。
cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
// 线程执行函数:核心逻辑是将自身设置为分离状态
void *thread_run( void * arg )
{
// pthread_self() 获取当前线程的pthread_t ID
// pthread_detach() 将当前线程标记为"分离状态"
// 分离后:线程退出时资源自动回收,主线程无法再调用pthread_join
pthread_detach(pthread_self());
// 打印传入的参数(主线程创建线程时传递的字符串)
printf("%s\n", (char*)arg);
// 线程正常退出,返回NULL(分离线程的返回值无意义,无法被pthread_join获取)
return NULL;
}
int main( void )
{
pthread_t tid; // 存储新创建线程的pthread库ID(用户层ID)
// 1. 创建新线程:
// 参数1:传出参数,存储线程ID;参数2:线程属性(NULL为默认);
// 参数3:线程执行函数;参数4:传递给线程函数的参数
if ( pthread_create(&tid, NULL, thread_run, "thread1 run...") != 0 ) {
printf("create thread error\n"); // 创建失败则打印错误
return 1; // 主线程退出,返回错误码1
}
int ret = 0; // 主线程最终返回值,默认0(成功)
// ========== 关键:sleep(1) 的作用 ==========
// 让主线程休眠1秒,确保子线程先执行到pthread_detach()完成分离
// 如果没有这行:主线程可能在子线程执行detach前就调用pthread_join,此时线程还是joinable状态,join会成功
// 加了这行:子线程已完成分离,后续join会失败(验证分离状态的特性)
sleep(1);
// 2. 尝试等待分离后的线程:
// pthread_join第二个参数传NULL,表示不关心线程返回值,只尝试回收资源
if ( pthread_join(tid, NULL ) == 0 ) {
// 执行到这里说明join成功(但分离线程不会走到这)
printf("pthread wait success\n");
ret = 0;
} else {
// 分离线程调用pthread_join会返回EINVAL错误,执行到这里
printf("pthread wait failed\n");
ret = 1;
}
return ret; // 返回最终状态(分离线程场景下会返回1)
}
5. Linux线程互斥
5.1进程线程间的互斥相关背景概念
临界资源:多线程执行流共享的资源就叫做临界资源
临界区:每个线程内部,访问临界资源的代码,就叫做临界区
互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用
原子性(后面讨论如何实现):不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成
5.2互斥量mutex
大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程无法获得这种变量。
但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互。
多个线程并发的操作共享变量,会带来一些问题。
| 类型 | 存储位置 | 访问范围 | 核心特点 |
|---|---|---|---|
| 局部变量 | 线程私有栈空间 | 仅所属线程可访问 | 天然线程安全,无并发问题 |
| 共享变量 | 进程堆 / 全局 / 静态区 | 所有线程均可访问 | 存在并发安全问题 |
cpp
// 操作共享变量会有问题的售票系统代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
int ticket = 100;
void* route(void* arg)
{
char* id = (char*)arg;
while (1) {
if (ticket > 0) {
usleep(1000);
printf("%s sells ticket:%d\n", id, ticket);
ticket--;
}
else {
break;
}
}
}
int main(void)
{
pthread_t t1, t2, t3, t4;
pthread_create(&t1, NULL, route, "thread 1");
pthread_create(&t2, NULL, route, "thread 2");
pthread_create(&t3, NULL, route, "thread 3");
pthread_create(&t4, NULL, route, "thread 4");
pthread_join(t1, NULL);
pthread_join(t2, NULL);
pthread_join(t3, NULL);
pthread_join(t4, NULL);
}
一次执行结果:
thread 4 sells ticket : 100
...
thread 4 sells ticket : 1
thread 2 sells ticket : 0
thread 1 sells ticket : -1
thread 3 sells ticket : -2
为什么可能无法获得争取结果?
if 语句判断条件为真以后,代码可以并发的切换到其他线程
usleep 这个模拟漫长业务的过程,在这个漫长的业务过程中,可能有很多个线程会进入该代码段
--ticket 操作本身就不是一个原子操作
cpp
取出ticket--部分的汇编代码
objdump -d a.out > test.objdump
152 40064b: 8b 05 e3 04 20 00 mov 0x2004e3(%rip),%eax # 600b34 <ticket>
153 400651: 83 e8 01 sub $0x1,%eax
154 400654: 89 05 da 04 20 00 mov %eax,0x2004da(%rip) # 600b34 <ticket>
-- 操作并不是原子操作,而是对应三条汇编指令:
load :将共享变量ticket从内存加载到寄存器中
update : 更新寄存器里面的值,执行-1操作
store :将新值,从寄存器写回共享变量ticket的内存地址
要解决以上问题,需要做到三点:
代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。
如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区。
如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。
要做到这三点,本质上就是需要一把锁。Linux上提供的这把锁叫互斥量。

5.3互斥量的接口
初始化互斥量
初始化互斥量有两种方法:
方法1,静态分配:
cpp
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
PTHREAD_MUTEX_INITIALIZER 是 pthread 库定义的宏,用于静态初始化一个默认属性的互斥锁 ,等价于通过 pthread_mutex_init 动态初始化且第二个参数传 NULL(默认属性)。
cpp
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict
attr);
参数:
mutex:要初始化的互斥量
attr:NULL
销毁互斥量
销毁互斥量需要注意:
使用 PTHREAD_ MUTEX_ INITIALIZER 初始化的互斥量不需要销毁
不要销毁一个已经加锁的互斥量
已经销毁的互斥量,要确保后面不会有线程再尝试加锁
这三句话是什么意思呢?让我们详细讲讲
一、规则 1:使用 PTHREAD_MUTEX_INITIALIZER 初始化的互斥量不需要销毁
- 核心原理
PTHREAD_MUTEX_INITIALIZER是静态初始化 (编译期完成),锁的内存位于全局 / 静态数据段(而非栈 / 堆),进程退出时操作系统会自动回收这部分内存资源;- 动态初始化的锁(
pthread_mutex_init)如果是堆上分配的,销毁是为了释放锁内部的动态资源(如内核对象),而静态初始化的锁无此类动态资源,因此 "销毁非必需"。
- 实操建议
- 可以销毁 :调用
pthread_mutex_destroy对静态初始化的锁无副作用(标准规定),为了代码规范性(统一初始化 / 销毁逻辑),建议还是调用; - 不销毁也安全:进程退出时系统会清理所有资源,不会造成内存泄漏。
cpp
// 静态初始化锁
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
int main() {
// 使用锁...
pthread_mutex_lock(&mutex);
pthread_mutex_unlock(&mutex);
// 即使是静态初始化,也建议销毁(无风险)
pthread_mutex_destroy(&mutex);
return 0;
}
二、规则 2:不要销毁一个已经加锁的互斥量
- 核心风险
- 互斥锁的核心状态是 "锁定 / 未锁定",如果锁处于加锁状态 时调用
pthread_mutex_destroy:- 会导致未定义行为(POSIX 标准未规定具体后果);
- 常见后果:程序崩溃、死锁、锁状态错乱(后续加锁 / 解锁异常);
- 本质原因:销毁操作会清理锁的内部结构,但此时还有线程持有锁,相当于 "拆了别人正在用的门"。
- 避坑方法
- 销毁前必须确保锁是未锁定状态;
- 如果是多线程场景,需先确保所有线程都已退出临界区、释放锁。
错误示例(禁止这样写):
cpp
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
int main() {
pthread_mutex_lock(&mutex); // 锁处于加锁状态
pthread_mutex_destroy(&mutex); // 错误:销毁已加锁的锁!
return 0;
}
正确示例:
cpp
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
int main() {
pthread_mutex_lock(&mutex);
pthread_mutex_unlock(&mutex); // 先解锁
pthread_mutex_destroy(&mutex); // 再销毁(安全)
return 0;
}
三、规则 3:已经销毁的互斥量,要确保后面不会有线程再尝试加锁
- 核心风险
- 销毁后的互斥锁内部结构已被清空,此时调用
pthread_mutex_lock/pthread_mutex_unlock:- 会触发未定义行为(如段错误、程序崩溃、锁失效);
- 相当于 "对着已经拆掉的门,尝试用钥匙开锁",完全无效且危险。
- 避坑方法
- 销毁时机:必须在所有线程都不再使用该锁后销毁(比如主线程等待所有子线程退出后,再销毁锁);
- 禁止复用:销毁后的锁不能再次初始化 / 使用,如需重新用锁,需重新定义或重新初始化。
错误示例(禁止这样写):
cpp
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
void *thread_func(void *arg) {
// 线程可能在锁销毁后仍尝试加锁(竞态条件)
pthread_mutex_lock(&mutex); // 危险:锁已被销毁!
pthread_mutex_unlock(&mutex);
return NULL;
}
int main() {
pthread_t tid;
pthread_create(&tid, NULL, thread_func, NULL);
pthread_mutex_destroy(&mutex); // 提前销毁锁
pthread_join(tid, NULL); // 线程可能还在尝试用锁
return 0;
}
正确示例:
cpp
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
void *thread_func(void *arg) {
pthread_mutex_lock(&mutex);
pthread_mutex_unlock(&mutex);
return NULL;
}
int main() {
pthread_t tid;
pthread_create(&tid, NULL, thread_func, NULL);
pthread_join(tid, NULL); // 先等线程退出,确保不再用锁
pthread_mutex_destroy(&mutex); // 再销毁(安全)
return 0;
}
互斥量加锁和解锁
cpp
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
返回值:成功返回0,失败返回错误号
调用 pthread_ lock 时,可能会遇到以下情况:
互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功
发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么pthread_ lock调用会陷入阻塞(执行流被挂起),等待互斥量解锁。
改进上面的售票系统:
cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <sched.h> // sched_yield() 函数的头文件(代码中注释掉了)
int ticket = 100; // 共享资源:总共有100张票,所有线程共享此变量
pthread_mutex_t mutex; // 定义互斥锁,用于保护ticket的并发操作
// 线程执行函数:模拟售票逻辑
void *route(void *arg)
{
char *id = (char*)arg; // 接收线程标识(如"thread 1")
while ( 1 ) { // 无限循环售票,直到票卖完
// ========== 加锁:进入临界区 ==========
// 所有线程竞争这把锁,同一时间只有一个线程能进入临界区
pthread_mutex_lock(&mutex);
if ( ticket > 0 ) { // 还有票可卖
usleep(1000); // 模拟售票耗时(微秒级,避免打印过快)
printf("%s sells ticket:%d\n", id, ticket); // 打印当前卖出的票号
ticket--; // 票数减1(共享变量操作,必须在临界区内)
pthread_mutex_unlock(&mutex); // 解锁:退出临界区(票未卖完时)
// sched_yield(); // 主动放弃CPU,让其他线程有机会执行(注释掉了)
} else { // 票已卖完
pthread_mutex_unlock(&mutex); // 注意:退出前必须解锁!否则会导致死锁
break; // 退出循环,线程结束
}
}
return NULL; // 线程正常退出
}
int main( void )
{
pthread_t t1, t2, t3, t4; // 定义4个线程ID
// 动态初始化互斥锁:第二个参数为NULL表示使用默认属性(普通锁)
pthread_mutex_init(&mutex, NULL);
// 创建4个售票线程,都执行route函数,传递不同的线程标识
pthread_create(&t1, NULL, route, "thread 1");
pthread_create(&t2, NULL, route, "thread 2");
pthread_create(&t3, NULL, route, "thread 3");
pthread_create(&t4, NULL, route, "thread 4");
// 等待4个线程全部执行完毕(阻塞主线程)
// 必须等待:否则主线程提前退出会销毁锁,导致子线程操作已销毁的锁
pthread_join(t1, NULL);
pthread_join(t2, NULL);
pthread_join(t3, NULL);
pthread_join(t4, NULL);
// 销毁互斥锁:此时所有线程已退出,锁处于未锁定状态,销毁安全
pthread_mutex_destroy(&mutex);
return 0;
}
核心注释补充说明
-
临界区的范围:
- 锁包裹的范围是
if (ticket > 0)整个分支,确保ticket的判断、打印、自减操作是原子的; - 若只锁
ticket--,会出现 "多个线程同时判断 ticket>0,导致卖出负数票" 的问题。
- 锁包裹的范围是
-
解锁的时机:
- 票未卖完时:解锁后线程会回到循环开头,再次竞争锁;
- 票卖完时:必须先解锁再 break,否则锁会一直被持有,其他线程永远拿不到锁(死锁)。
-
usleep(1000)的作用:- 模拟真实场景中的售票耗时,让多线程的竞争更明显;
- 如果去掉这个延时,可能出现单个线程快速卖完所有票的情况(CPU 调度问题)。
-
sched_yield()的作用(注释掉的部分):- 主动放弃 CPU 使用权,让其他线程优先执行,能让 4 个线程更均匀地售票;
- 不加的话,可能出现某一个线程连续卖多张票的情况(不影响结果正确性,只是调度策略问题)。
5.4互斥量实现原理探究
经过上面的例子,大家已经意识到单纯的 i++ 或者 ++i 都不是原子的,有可能会有数据一致性问题
为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的 总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。 现在我们把lock和unlock的伪代码改一下

cpp
lock:
movb $0, %al ; 将0加载到al寄存器
xchgb %al, mutex ; 原子交换:把al的值(0)和mutex的值交换,原mutex的值存入al
if (al寄存器的内容 > 0) {
return 0; ; 交换后al>0 → 原mutex是1(未锁),加锁成功
} else {
挂起等待; ; 交换后al=0 → 原mutex是0(已锁),加锁失败
goto lock; ; 等待后重试(或进入阻塞队列)
xchgb的关键作用 :这是一条原子交换指令,CPU 会保证整个交换过程不可被中断,这是互斥锁能安全工作的硬件基础。- 判断逻辑 :
- 若交换后
al > 0,说明原mutex是1(未锁),当前线程成功拿到锁,直接返回。 - 若交换后
al = 0,说明原mutex是0(已被其他线程占用),当前线程需要挂起等待,之后重试。
- 若交换后
cpp
unlock:
movb $1, mutex ; 直接把mutex的值设回1(表示锁已释放)
唤醒等待Mutex的线程; ; 通知内核,唤醒所有在这个锁上等待的线程
return 0; ; 解锁成功
- 解锁操作 :直接将
mutex置为1,表示锁已可用。 - 唤醒机制:解锁后必须唤醒等待队列中的线程,让它们重新竞争锁,否则会导致线程永久阻塞。
6.可重入VS线程安全
6.1概念
线程安全:多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作,并且没有锁保护的情况下,会出现该问题。
重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数。
6.2常见的线程不安全的情况
不保护共享变量的函数
函数状态随着被调用,状态发生变化的函数
返回指向静态变量指针的函数
调用线程不安全函数的函数
一、第一类:不保护共享变量的函数
- 风险根源
函数内部操作全局 / 静态共享变量 ,但未加锁保护,多线程并发调用时会触发数据竞争(如之前讲的 ticket-- 问题)。
-
典型示例
// 线程不安全的计数器函数(未保护共享变量)
int g_count = 0;
void count() {
g_count++; // 非原子操作,多线程调用会计数丢失
}// 多线程调用:结果远小于预期
void *thread_func(void *arg) {
for (int i=0; i<1000; i++) count();
return NULL;
} -
解决方案
给共享变量的操作加互斥锁,将临界区保护起来:
int g_count = 0;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
// 线程安全的计数器函数
void count() {
pthread_mutex_lock(&mutex);
g_count++;
pthread_mutex_unlock(&mutex);
}
二、第二类:函数状态随调用发生变化的函数
- 风险根源
函数依赖自身的内部状态(如静态变量、全局变量),每次调用会修改状态,多线程并发调用时会导致状态错乱(后调用的线程覆盖先调用线程的状态)。
-
典型示例:简易的 "累加器函数"
// 线程不安全:静态变量保存累加状态
int add(int num) {
static int sum = 0; // 静态变量:状态随调用变化
sum += num;
return sum;
}// 线程A调用 add(1),线程B调用 add(2):
// 可能出现 sum 最终为 2(而非 3),因为状态被覆盖 -
解决方案
-
方案 1:将状态从 "静态 / 全局" 改为线程私有(传入参数或用 TLS);
-
方案 2:加锁保护状态修改(但会降低并发效率)。
// 方案1:状态由调用者传入(推荐)
int add(int num, int *sum) {
*sum += num; // 状态存在调用者的变量中,线程私有
return *sum;
}// 方案2:加锁保护(兼容旧代码)
static int sum = 0;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
int add(int num) {
pthread_mutex_lock(&mutex);
sum += num;
pthread_mutex_unlock(&mutex);
return sum;
}
三、第三类:返回指向静态变量指针的函数
- 风险根源
函数返回静态变量的指针(静态变量存储在数据段,所有线程共享),多线程调用时会导致:
- 后调用的线程覆盖静态变量的值,先调用的线程拿到的指针指向错误数据;
- 指针指向的内存被多个线程同时读写,引发数据竞争。
-
典型示例:C 标准库的
strtok/asctime// 示例1:strtok(内部用静态变量保存分割位置)
char *str = "a,b,c";
// 线程A调用:strtok(str, ",") → 返回"a",静态变量记录下一个位置
// 线程B调用:strtok("x,y,z", ",") → 覆盖静态变量,线程A后续调用会拿到错误结果// 示例2:自定义函数返回静态变量指针
char *get_msg() {
static char buf[1024]; // 静态缓冲区,所有线程共享
snprintf(buf, sizeof(buf), "hello");
return buf; // 返回共享缓冲区的指针
} -
解决方案
-
方案 1:改用 "可重入版本"(如
strtok_r替代strtok,asctime_r替代asctime); -
方案 2:让调用者传入私有缓冲区,避免使用静态变量。
// 方案1:使用可重入版本 strtok_r
char *str = "a,b,c";
char *saveptr; // 线程私有变量,保存分割位置
strtok_r(str, ",", &saveptr);// 方案2:调用者传入缓冲区
void get_msg(char *buf, int buf_size) {
snprintf(buf, buf_size, "hello"); // 写入调用者的私有缓冲区
}
四、第四类:调用线程不安全函数的函数
- 风险根源
函数本身看似无问题,但内部调用了上述三类线程不安全函数,导致 "间接不安全"。
-
典型示例
// 线程不安全的函数:内部调用了返回静态变量的 asctime
char *format_time(time_t t) {
struct tm *tm = localtime(&t);
return asctime(tm); // asctime 返回静态变量指针 → 整个函数不安全
}// 多线程调用 format_time:线程A和B拿到的指针指向同一个静态缓冲区,数据被覆盖
-
解决方案
-
替换内部调用的不安全函数为可重入版本;
-
对整个函数加锁,保证同一时间只有一个线程调用。
// 方案1:改用可重入版本 asctime_r
char *format_time(time_t t, char *buf, int buf_size) {
struct tm tm;
localtime_r(&t, &tm); // 可重入版本
asctime_r(&tm, buf); // 写入调用者的缓冲区
return buf;
}// 方案2:加锁保护(简单但并发效率低)
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
char *format_time(time_t t) {
pthread_mutex_lock(&mutex);
struct tm *tm = localtime(&t);
char *ret = asctime(tm);
pthread_mutex_unlock(&mutex);
return ret;
}
常见的线程安全的情况
每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的
类或者接口对于线程来说都是原子操作
多个线程之间的切换不会导致该接口的执行结果存在二义性
常见不可重入的情况
调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的
调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构
可重入函数体内使用了静态的数据结构
一、场景 1:调用 malloc/free → 线程不安全(全局链表管理堆)
- 风险根源
malloc/free 的底层实现依赖全局链表管理堆内存块:
malloc:遍历全局空闲内存链表,找到合适的块分配,同时修改链表(删除节点);free:将释放的内存块归还给空闲链表,修改链表(插入节点);- 链表的遍历 / 修改是非原子操作 ,多线程并发调用
malloc/free时,会导致链表节点错乱(如内存泄漏、程序崩溃)。
-
直观示例(风险场景)
void *thread_func(void *arg) {
// 多线程并发调用malloc:全局链表被同时修改
int p = (int)malloc(sizeof(int));
free(p); // 并发free:同样会破坏全局链表
return NULL;
}
- 极端情况:线程 A 执行
malloc遍历链表到一半时被切换,线程 B 执行free修改了该链表,线程 A 恢复后会访问无效节点,触发段错误。
- 解决方案
- 现代 glibc 的
malloc已做线程安全优化(每个线程有私有缓存 arena),但核心全局链表操作仍会加锁(隐式锁),无需手动处理; - 若需更高性能:使用线程私有内存池(如每个线程预分配一块内存,内部自行管理,减少全局
malloc调用)。
二、场景 2:调用标准 I/O 库函数 → 不可重入(全局数据结构)
- 风险根源
标准 I/O 库(printf/fscanf/fwrite 等)的底层实现依赖全局 / 静态缓冲区:
- 例如
printf会先将数据写入全局缓冲区,再批量刷新到终端; fgets会用静态变量保存文件读取位置;- 多线程并发调用时,全局缓冲区会被多个线程同时写入,导致输出错乱(如两个线程的打印内容混在一起)。
-
典型示例(输出错乱)
void *thread1(void *arg) {
for (int i=0; i<5; i++) {
printf("thread1: %d\n", i); // 依赖全局输出缓冲区
}
return NULL;
}void *thread2(void *arg) {
for (int i=0; i<5; i++) {
printf("thread2: %d\n", i); // 并发写入全局缓冲区
}
return NULL;
}
- 预期输出:thread1 和 thread2 的内容分行打印;
- 实际输出:可能出现
thread1: 0thread2: 0这类混行(全局缓冲区被同时写入)。
- 解决方案
-
方案 1:手动加锁保护 I/O 调用(推荐):
pthread_mutex_t io_mutex = PTHREAD_MUTEX_INITIALIZER; void safe_printf(const char *fmt, ...) { va_list args; pthread_mutex_lock(&io_mutex); // 加锁保护全局缓冲区 va_start(args, fmt); vprintf(fmt, args); va_end(args); pthread_mutex_unlock(&io_mutex); } -
方案 2:使用无缓冲 I/O(如
write系统调用),绕过标准 I/O 的全局缓冲区。
三、场景 3:可重入函数体内使用静态数据结构 → 失去可重入性
- 核心概念回顾
- 可重入函数:函数的执行结果仅依赖输入,无全局 / 静态状态,并发调用时结果唯一;
- 静态数据结构的破坏作用:一旦函数内部使用静态变量 / 结构体,就引入了 "共享状态",可重入性直接丧失。
-
典型示例(伪可重入函数)
// 看似可重入,实则因静态变量不可重入
char *safe_strcpy(char *dest, const char *src) {
static char buf[1024]; // 静态缓冲区(共享)
strcpy(buf, src); // 写入共享缓冲区
strcpy(dest, buf); // 复制到目标地址
return dest;
}
- 问题:线程 A 调用
safe_strcpy(d1, "a"),线程 B 同时调用safe_strcpy(d2, "b")→ 静态缓冲区buf被线程 B 覆盖,线程 A 的d1最终写入的是"b",结果错误。
- 解决方案
-
彻底移除静态数据结构,改为调用者传入私有缓冲区 :
// 真正可重入的strcpy(无静态状态) char *real_safe_strcpy(char *dest, const char *src, char *tmp_buf, int buf_size) { strncpy(tmp_buf, src, buf_size-1); // 写入调用者的私有缓冲区 tmp_buf[buf_size-1] = '\0'; strcpy(dest, tmp_buf); return dest; }
常见可重入的情况
不使用全局变量或静态变量
不使用用malloc或者new开辟出的空间
不调用不可重入函数
不返回静态或全局数据,所有数据都有函数的调用者提供
使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据
可重入与线程安全联系
函数是可重入的,那就是线程安全的
函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题
如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。
可重入与线程安全区别
可重入函数是线程安全函数的一种
线程安全不一定是可重入的,而可重入函数则一定是线程安全的。
如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的
一、先明确两个核心定义(从本质区分)
| 维度 | 可重入函数(Reentrant) | 线程安全函数(Thread-safe) |
|---|---|---|
| 核心目标 | 允许函数被 "中断后再次调用"(如信号处理中调用自身) | 允许函数被多线程并发调用,结果无歧义 |
| 核心要求 | 1. 无全局 / 静态状态;2. 不使用共享资源;3. 所有状态由参数传入 | 1. 共享资源被正确保护(如加锁);2. 并发调用无数据竞争 |
| 依赖手段 | 完全消除共享状态(天然安全) | 可通过加锁保护共享状态(人为保障安全) |
二、关系推导:你总结的结论为什么成立?
- 可重入函数 → 一定是线程安全的
可重入函数的核心是「无任何共享状态」(全局 / 静态 / 共享资源),所有操作都依赖调用者传入的参数(线程私有):
- 多线程并发调用时,每个线程操作的都是自己的私有数据,不存在任何共享资源,自然不会有数据竞争;
- 举例:
int add(int a, int b) { return a+b; }(无状态、可重入)→ 多线程调用结果永远是a+b,线程安全。
- 线程安全函数 → 不一定是可重入的
线程安全函数可以通过「加锁保护共享资源」实现,但加锁会破坏可重入性:
- 线程安全只要求 "多线程并发调用安全",但不要求 "中断后重入安全";
- 一旦函数内加锁,若函数被中断后再次调用自身(重入),会因锁未释放导致死锁,失去可重入性。
三、关键案例:加锁的线程安全函数 → 不可重入(死锁演示)
#include <stdio.h>
#include <pthread.h>
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
int g_count = 0;
// 线程安全函数(加锁保护共享变量),但不可重入
void thread_safe_but_not_reentrant(int num) {
pthread_mutex_lock(&mutex); // 加锁保护临界区
g_count += num;
printf("g_count = %d\n", g_count);
// 模拟"重入调用"(比如信号处理函数调用该函数)
if (num < 5) {
thread_safe_but_not_reentrant(num + 1); // 重入自身
}
pthread_mutex_unlock(&mutex); // 解锁
}
int main() {
// 单线程调用也会死锁!因为重入时锁已被持有
thread_safe_but_not_reentrant(1);
return 0;
}
死锁原因分析:
- 第一次调用
thread_safe_but_not_reentrant(1):成功加锁,g_count = 1; - 执行到
num < 5,递归调用thread_safe_but_not_reentrant(2); - 第二次调用尝试加锁:此时锁已被第一次调用持有,当前线程阻塞等待解锁;
- 但第一次调用要等第二次调用返回后才会解锁,形成「自己等自己」的死锁。
对比:可重入版本(无锁、无共享状态)
// 可重入函数(无全局状态、无锁)→ 天然线程安全
int reentrant_and_thread_safe(int num, int *count) {
*count += num;
printf("count = %d\n", *count);
if (num < 5) {
reentrant_and_thread_safe(num + 1, count); // 重入无风险
}
return *count;
}
// 调用时传入线程私有变量(栈变量)
int main() {
int count = 0; // 私有状态,非共享
reentrant_and_thread_safe(1, &count); // 无死锁,结果正确
return 0;
}
**7.**常见锁概念
7.1死锁
死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态。
7.2死锁四个必要条件
互斥条件:一个资源每次只能被一个执行流使用
请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放
不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺
循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系
一、条件 1:互斥条件(资源排他性)
定义
一个资源同一时间只能被一个执行流(线程 / 进程)占用,其他执行流必须等待,直到资源被释放。这是资源的 "天然属性"(比如互斥锁、打印机、唯一的文件句柄),也是死锁的基础条件。
示例
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
// 线程A和B竞争同一把锁,同一时间只有一个能拿到 → 互斥条件成立
void *threadA(void *arg) { pthread_mutex_lock(&mutex); }
void *threadB(void *arg) { pthread_mutex_lock(&mutex); }
如何打破?
- 核心思路:让资源变为「可共享访问」(消除排他性);
- 适用场景:仅适用于 "只读资源"(如全局常量),读写资源无法打破(必须互斥);
- 举例:将共享变量设为
const,所有线程只读,无需互斥锁,自然无死锁。
二、条件 2:请求与保持条件(占着资源等新资源)
定义
执行流已经持有部分资源,又请求新的资源;若新资源被占用,执行流不会释放已持有的资源,而是阻塞等待。这是死锁的 "核心诱因"------ 执行流不放手已有资源,又抢新资源。
示例(线程 A 占锁 1 等锁 2,线程 B 占锁 2 等锁 1)
pthread_mutex_t m1 = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t m2 = PTHREAD_MUTEX_INITIALIZER;
// 线程A:持有m1,请求m2 → 阻塞时不释放m1
void *threadA(void *arg) {
pthread_mutex_lock(&m1); // 拿到m1
pthread_mutex_lock(&m2); // 等m2,阻塞时仍持有m1
}
// 线程B:持有m2,请求m1 → 阻塞时不释放m2
void *threadB(void *arg) {
pthread_mutex_lock(&m2); // 拿到m2
pthread_mutex_lock(&m1); // 等m1,阻塞时仍持有m2
}
如何打破?
-
核心思路:「一次性申请所有资源」或「申请不到新资源时释放已有资源」;
- 方案 1:预申请所有资源(比如线程启动时一次性加所有锁,不中途申请);
- 方案 2:申请新资源失败时,主动释放已持有的资源,稍后重试。
-
示例优化(方案 1:一次性申请):
// 封装锁申请函数,一次性申请m1和m2,失败则都释放 int lock_both(pthread_mutex_t *m1, pthread_mutex_t *m2) { if (pthread_mutex_lock(m1) != 0) return -1; if (pthread_mutex_lock(m2) != 0) { pthread_mutex_unlock(m1); // 申请m2失败,释放m1 return -1; } return 0; }
三、条件 3:不剥夺条件(资源不能被强行拿走)
定义
执行流已获得的资源,在未主动释放前,其他执行流不能强行剥夺,只能等待其自愿释放。这是死锁的 "保障条件"------ 如果资源能被强行拿走,阻塞的执行流可以抢回资源,死锁就不会发生。
示例
线程 A 持有锁 1,线程 B 请求锁 1 被阻塞;此时无法强行从线程 A 手中 "抢走" 锁 1,只能等 A 释放 → 不剥夺条件成立。
如何打破?
-
核心思路:让资源支持「可剥夺」(超时释放 / 主动放弃);
-
具体方案:
- 使用带超时的锁(
pthread_mutex_timedlock):申请资源超时后,自动放弃申请,释放已有资源; - 资源设置 "抢占标记":高优先级执行流可剥夺低优先级执行流的资源(如操作系统的进程调度)。
- 使用带超时的锁(
-
示例(超时锁打破不剥夺条件):
struct timespec ts; clock_gettime(CLOCK_REALTIME, &ts); ts.tv_sec += 1; // 超时时间1秒 // 线程A申请m2超时后,释放已持有的m1,避免死锁 pthread_mutex_lock(&m1); if (pthread_mutex_timedlock(&m2, &ts) != 0) { pthread_mutex_unlock(&m1); // 申请失败,释放已有资源 printf("申请m2超时,释放m1\n"); }
四、条件 4:循环等待条件(执行流互相等对方的资源)
定义
多个执行流之间形成 "环形依赖":执行流 A 等执行流 B 的资源,B 等 C 的资源,...,最后一个执行流等 A 的资源,形成闭环。这是死锁的 "最终形态"------ 前三个条件满足后,循环等待会直接触发死锁。
示例(环形依赖)
线程A:持有m1 → 请求m2
线程B:持有m2 → 请求m3
线程C:持有m3 → 请求m1
→ 形成 A→B→C→A 的循环等待,死锁
如何打破?
-
核心思路:「统一资源申请顺序」(让所有执行流按相同的顺序申请资源,消除环形依赖);
-
具体方案:给所有资源编号(如 m1=1,m2=2,m3=3),所有执行流必须按 "从小到大" 的顺序申请资源。
-
示例优化(统一顺序):
// 规定:所有线程必须先申请m1(编号1),再申请m2(编号2),最后申请m3(编号3) void *threadA(void *arg) { pthread_mutex_lock(&m1); pthread_mutex_lock(&m2); } void *threadB(void *arg) { pthread_mutex_lock(&m1); // 不再先申请m2,按顺序来 pthread_mutex_lock(&m2); pthread_mutex_lock(&m3); } void *threadC(void *arg) { pthread_mutex_lock(&m1); pthread_mutex_lock(&m3); } // 无环形依赖,即使阻塞也只会单向等待,不会死锁
7.3避免死锁
破坏死锁的四个必要条件
加锁顺序一致
避免锁未释放的场景
资源一次性分配
7.4避免死锁算法
死锁检测算法(了解)
银行家算法(了解)
8. Linux线程同步
8.1条件变量
当一个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。
例如一个线程访问队列时,发现队列为空,它只能等待,直到其它线程将一个节点添加到队列中。这种情况就需要用到条件变量。
8.2同步概念与竞态条件
同步:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做同步
竞态条件:因为时序问题,而导致程序异常,我们称之为竞态条件。在线程场景下,这种问题也不难理解
8.3条件变量函数 初始化
cpp
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict
attr);
参数:
cond:要初始化的条件变量
attr:NULL
8.4销毁
cpp
int pthread_cond_destroy(pthread_cond_t *cond)
8.5等待条件满足
cpp
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
参数:
cond:要在这个条件变量上等待
mutex:互斥量
注意:pthread_cond_wait会自动解锁,被唤醒后重新加锁
条件变量的核心作用是 "等待条件成立",必须和互斥锁配合(避免竞态条件),典型场景是生产者 - 消费者模型
也就是说当条件变量满足时也就是调用了pthread_cond_signal(&cond);,pthread_cond_wait会自动解锁,然后重新加锁,往下面执行.
8.6唤醒等待
cpp
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast是 POSIX 线程库中唤醒所有等待在某个条件变量上的线程 的函数,是条件变量核心操作之一(另一个是唤醒单个线程的int pthread_cond_signal)
简单案例:
cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
pthread_cond_t cond; // 定义条件变量:用于线程间的同步通知(t2通知t1执行)
pthread_mutex_t mutex; // 定义互斥锁:配合条件变量使用,避免竞态条件
// 线程1执行函数:等待条件变量被唤醒,然后打印"活动"
void *r1( void *arg )
{
while ( 1 ) { // 无限循环:持续等待通知
// ========== 核心:条件变量等待 ==========
// 1. 调用时会自动释放互斥锁mutex,让其他线程(如t2)能操作共享资源/发送信号
// 2. 被pthread_cond_signal唤醒后,会重新获取互斥锁,再继续执行
// 3. 必须传入互斥锁:条件变量的等待操作依赖互斥锁保证原子性
pthread_cond_wait(&cond, &mutex);
// 被唤醒后执行:打印"活动"
printf("活动\n");
}
}
// 线程2执行函数:每隔1秒发送一次信号,唤醒线程1
void *r2(void *arg )
{
while ( 1 ) { // 无限循环:持续发送通知
// 发送信号:唤醒至少一个等待在cond上的线程(这里只有t1)
// 注意:发送信号时不需要持有互斥锁(但持有也可以,不影响)
pthread_cond_signal(&cond);
sleep(1); // 休眠1秒:控制通知频率,让t1每秒被唤醒一次
}
}
int main( void )
{
pthread_t t1, t2; // 定义两个线程ID:t1(等待线程)、t2(通知线程)
// 动态初始化条件变量:第二个参数NULL表示使用默认属性(进程私有、系统时钟)
pthread_cond_init(&cond, NULL);
// 动态初始化互斥锁:第二个参数NULL表示使用默认属性(普通互斥锁)
pthread_mutex_init(&mutex, NULL);
// 创建线程1:执行r1函数,负责等待条件变量通知
pthread_create(&t1, NULL, r1, NULL);
// 创建线程2:执行r2函数,负责发送条件变量信号
pthread_create(&t2, NULL, r2, NULL);
// 等待线程结束(这里线程是无限循环,实际不会执行到这)
// 作用:阻塞主线程,避免主线程提前销毁锁和条件变量
pthread_join(t1, NULL);
pthread_join(t2, NULL);
// 销毁互斥锁和条件变量(实际不会执行到,仅为代码规范)
// 销毁前提:必须确保无线程在使用该锁/条件变量
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cond);
return 0;
}
为什么pthread_cond_wait 需要互斥量**?**
条件等待是线程间同步的一种手段,如果只有一个线程,条件不满足,一直等下去都不会满足,所以必须要有一个线程通过某些操作,改变共享变量,使原先不满足的条件变得满足,并且友好的通知等待在条件变量上的线程。
条件不会无缘无故的突然变得满足了,必然会牵扯到共享数据的变化。所以一定要用互斥锁来保护。没有互斥锁就无法安全的获取和修改共享数据。

- 左侧场景:线程持有锁并进入
wait
- 线程先获取了互斥锁(
mutex)。 - 然后调用
pthread_cond_wait(&cond, &mutex)进入等待。 - 这里的关键是:
pthread_cond_wait会原子地释放互斥锁,并将线程挂入条件变量的等待队列。这一步是安全的,为其他线程让出了锁。
- 右侧场景:线程持有锁并尝试
signal
- 另一个线程(或同一个线程)尝试发送唤醒信号
pthread_cond_signal(&cond)。 - 但此时它仍然持有互斥锁,没有释放。
- 这就导致了问题:
- 等待在
cond上的线程(左侧)被唤醒后,需要重新获取互斥锁才能继续执行。 - 但锁被右侧发送信号的线程持有,且该线程没有释放,导致等待线程永远无法拿到锁,形成死锁。
- 等待在
- 死锁的根本原因
这个死锁的本质是:发送信号的线程在持有互斥锁的情况下调用了 pthread_cond_signal。
虽然 POSIX 标准允许在持有锁时调用 signal,但这是一种非常危险的做法。在这个场景下,它直接导致了:
- 等待线程被唤醒后,无法获取锁,只能继续阻塞。
- 发送信号的线程也因为后续逻辑(图中 "被锁挡住了,没办法执行")无法释放锁。
- 最终形成 "你等我解锁,我等你执行" 的死锁局面。
- 正确的做法
为了避免这种死锁,必须遵循以下原则:
- 在调用
pthread_cond_signal或pthread_cond_broadcast之前,先释放互斥锁。 - 或者,确保发送信号的线程在发送完信号后,能尽快释放锁。
正确的代码流程应该是:
// 错误:持有锁发送信号
// pthread_mutex_lock(&mutex);
// pthread_cond_signal(&cond);
// ... 其他操作 ...
// pthread_mutex_unlock(&mutex);
// 正确:先解锁,再发送信号
pthread_mutex_lock(&mutex);
// ... 修改共享变量 ...
pthread_mutex_unlock(&mutex); // 先释放锁
pthread_cond_signal(&cond); // 再发送信号
按照上面的说法,我们设计出如下的代码:先上锁,发现条件不满足,解锁,然后等待在条件变量上不就行了,如下代码:
cpp
// 错误的设计
pthread_mutex_lock(&mutex);
while (condition_is_false) {
pthread_mutex_unlock(&mutex);
//解锁之后,等待之前,条件可能已经满足,信号已经发出,但是该信号可能被错过
pthread_cond_wait(&cond);
pthread_mutex_lock(&mutex);
}
pthread_mutex_unlock(&mutex);
由于解锁和等待不是原子操作。调用解锁之后, pthread_cond_wait 之前,如果已经有其他线程获取到互斥量,摒弃条件满足,发送了信号,那么 pthread_cond_wait 将错过这个信号,可能会导致线程永远阻塞在这个 pthread_cond_wait 。所以解锁和等待必须是一个原子操作。
int pthread_cond_wait(pthread_cond_ t *cond,pthread_mutex_ t * mutex); 进入该函数后,
会去看条件量等于0不?等于,就把互斥量变成1,直到cond_ wait返回,把条件量改成1,把互斥量恢复成原样。
8.7条件变量使用规范
等待条件代码
cpp
pthread_mutex_lock(&mutex);
while (条件为假)
pthread_cond_wait(cond, mutex);
修改条件
pthread_mutex_unlock(&mutex);
给条件发送信号代码
cpp
pthread_mutex_lock(&mutex);
设置条件为真
pthread_cond_signal(cond);
pthread_mutex_unlock(&mutex);
**9.**生产者消费者模型
9.1为何要使用生产者消费者模型
生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。这个阻塞队列就是用来给生产者和消费者解耦的。
9.2生产者消费者模型优点
解耦
支持并发
支持忙闲不均

9.3基于BlockingQueue的生产者消费者模型
BlockingQueue
在多线程编程中阻塞队列(Blocking Queue)是一种常用于实现生产者和消费者模型的数据结构。其与普通的队列区别在于,当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中被放入了元素;当队列满时,往队列里存放元素的操作也会被阻塞,直到有元素被从队列中取出(以上的操作都是基于不同的线程来说的,线程在对阻塞队列进程操作时会被阻塞)

9.4C++ queue****模拟阻塞队列的生产消费模型
cpp
#include <iostream>
#include <queue>
#include <stdlib.h>
#include <pthread.h>
#include <time.h> // 补充time头文件(srand需要)
#define NUM 8 // 阻塞队列的默认容量:最多存放8个数据
// 阻塞队列类:封装生产者-消费者模型的核心逻辑,线程安全
class BlockQueue{
private:
std::queue<int> q; // 底层容器:存储数据的队列(共享资源)
int cap; // 队列容量:控制队列最大存储数量
pthread_mutex_t lock; // 互斥锁:保护队列的并发访问(临界区)
pthread_cond_t full; // 条件变量:队列满时,阻塞生产者
pthread_cond_t empty; // 条件变量:队列空时,阻塞消费者
private:
// 私有方法:加锁(封装互斥锁操作,简化代码)
void LockQueue()
{
pthread_mutex_lock(&lock);
}
// 私有方法:解锁(封装互斥锁操作)
void UnLockQueue()
{
pthread_mutex_unlock(&lock);
}
// 私有方法:生产者等待(队列满时,阻塞生产者)
void ProductWait()
{
// 等待full条件变量,同时释放锁;被唤醒后重新加锁
pthread_cond_wait(&full, &lock);
}
// 私有方法:消费者等待(队列空时,阻塞消费者)
void ConsumeWait()
{
// 等待empty条件变量,同时释放锁;被唤醒后重新加锁
pthread_cond_wait(&empty, &lock);
}
// 私有方法:通知生产者(队列有空位时,唤醒阻塞的生产者)
void NotifyProduct()
{
pthread_cond_signal(&full);
}
// 私有方法:通知消费者(队列有数据时,唤醒阻塞的消费者)
void NotifyConsume()
{
pthread_cond_signal(&empty);
}
// 私有方法:判断队列是否为空(仅在加锁后调用)
bool IsEmpty()
{
return ( q.size() == 0 ? true : false );
}
// 私有方法:判断队列是否已满(仅在加锁后调用)
bool IsFull()
{
return ( q.size() == cap ? true : false );
}
public:
// 构造函数:初始化队列容量、互斥锁、条件变量
BlockQueue(int _cap = NUM):cap(_cap)
{
pthread_mutex_init(&lock, NULL); // 初始化互斥锁(默认属性)
pthread_cond_init(&full, NULL); // 初始化full条件变量
pthread_cond_init(&empty, NULL); // 初始化empty条件变量
}
// 生产者接口:向队列中放入数据(线程安全)
void PushData(const int &data)
{
LockQueue(); // 加锁:保护队列操作(临界区开始)
// 循环检查队列是否满:避免虚假唤醒(必须用while,不能用if)
while(IsFull()){
NotifyConsume(); // 通知消费者:队列满了,快来消费
std::cout << "queue full, notify consume data, product stop." << std::endl;
ProductWait(); // 生产者阻塞:等待队列有空位
}
q.push(data); // 队列未满,放入数据
// NotifyConsume(); // 可选:放入数据后主动通知消费者(本例注释掉,消费者会自行检测)
UnLockQueue(); // 解锁:临界区结束
}
// 消费者接口:从队列中取出数据(线程安全)
void PopData(int &data)
{
LockQueue(); // 加锁:保护队列操作(临界区开始)
// 循环检查队列是否空:避免虚假唤醒(必须用while,不能用if)
while(IsEmpty()){
NotifyProduct(); // 通知生产者:队列空了,快来生产
std::cout << "queue empty, notify product data, consume stop." << std::endl;
ConsumeWait(); // 消费者阻塞:等待队列有数据
}
data = q.front(); // 取出队列头部数据
q.pop(); // 删除队列头部数据
// NotifyProduct(); // 可选:取出数据后主动通知生产者(本例注释掉,生产者会自行检测)
UnLockQueue(); // 解锁:临界区结束
}
// 析构函数:释放互斥锁和条件变量资源
~BlockQueue()
{
pthread_mutex_destroy(&lock); // 销毁互斥锁
pthread_cond_destroy(&full); // 销毁full条件变量
pthread_cond_destroy(&empty); // 销毁empty条件变量
}
};
// 消费者线程执行函数:循环从阻塞队列取数据
void *consumer(void *arg)
{
BlockQueue *bqp = (BlockQueue*)arg; // 接收阻塞队列对象指针
int data; // 存储取出的数据
for( ; ; ){ // 无限循环消费
bqp->PopData(data); // 从队列取数据(队列为空时阻塞)
std::cout << "Consume data done : " << data << std::endl;
}
}
// 生产者线程执行函数:循环向阻塞队列放数据(生产速度更快)
void *producter(void *arg)
{
BlockQueue *bqp = (BlockQueue*)arg; // 接收阻塞队列对象指针
srand((unsigned long)time(NULL)); // 初始化随机数种子
for( ; ; ){ // 无限循环生产
int data = rand() % 1024; // 生成0~1023的随机数作为生产数据
bqp->PushData(data); // 向队列放数据(队列满时阻塞)
std::cout << "Prodoct data done: " << data << std::endl;
// sleep(1); // 注释掉后生产者速度远快于消费者,会触发队列满的阻塞逻辑
}
}
int main()
{
BlockQueue bq; // 创建阻塞队列对象(默认容量8)
pthread_t c,p; // 定义消费者、生产者线程ID
// 创建消费者线程:执行consumer函数,传入阻塞队列对象地址
pthread_create(&c, NULL, consumer, (void*)&bq);
// 创建生产者线程:执行producter函数,传入阻塞队列对象地址
pthread_create(&p, NULL, producter, (void*)&bq);
// 等待线程结束(线程是无限循环,实际不会执行到这)
// 作用:阻塞主线程,避免主线程提前销毁阻塞队列对象
pthread_join(c, NULL);
pthread_join(p, NULL);
return 0;
}
10.POSIX****信号量
POSIX信号量和SystemV信号量作用相同,都是用于同步操作,达到无冲突的访问共享资源目的。 但POSIX可以用于线程间同步。
初始化信号量
cpp
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
参数:
pshared:0表示线程间共享,非零表示进程间共享
value:信号量初始值
销毁信号量
cpp
int sem_destroy(sem_t *sem);
等待信号量
cpp
功能:等待信号量,会将信号量的值减1
int sem_wait(sem_t *sem); //P()
发布信号量
cpp
功能:发布信号量,表示资源使用完毕,可以归还资源了。将信号量值加1。
int sem_post(sem_t *sem);//V()
上一节生产者-消费者的例子是基于queue的,其空间可以动态分配,现在基于固定大小的环形队列重写这个程序 (POSIX信号量):
11.0基于环形队列的生产消费模型
环形队列采用数组模拟,用模运算来模拟环状特性

环形结构起始状态和结束状态都是一样的,不好判断为空或者为满,所以可以通过加计数器或者标记位来判断满或者空。另外也可以预留一个空的位置,作为满的状态

但是我们现在有信号量这个计数器,就很简单的进行多线程间的同步过程
cpp
#include <iostream>
#include <vector>
#include <stdlib.h>
#include <semaphore.h> // 信号量头文件(POSIX信号量)
#include <pthread.h>
#include <time.h> // 补充time头文件(srand需要)
#define NUM 16 // 环形队列的默认容量:最多存放16个数据
// 环形队列类:基于信号量实现线程安全的生产者-消费者模型
// 核心:用信号量管理"可用数据数"和"可用空间数",无需互斥锁+条件变量的复杂协作
class RingQueue{
private:
std::vector<int> q; // 底层容器:模拟环形队列的连续内存(固定大小)
int cap; // 队列容量:环形队列的最大存储数量
sem_t data_sem; // 数据信号量:表示队列中"可用数据的数量"(初始0)
sem_t space_sem; // 空间信号量:表示队列中"可用空间的数量"(初始cap)
int consume_step; // 消费者下标:记录消费者下次取数据的位置(环形偏移)
int product_step; // 生产者下标:记录生产者下次放数据的位置(环形偏移)
public:
// 构造函数:初始化环形队列、信号量、下标
RingQueue(int _cap = NUM):q(_cap),cap(_cap)
{
// 初始化数据信号量:
// 参数1:信号量对象;参数2:0表示线程间共享(非进程间);参数3:初始值0(无可用数据)
sem_init(&data_sem, 0, 0);
// 初始化空间信号量:初始值cap(队列初始为空,有cap个可用空间)
sem_init(&space_sem, 0, cap);
consume_step = 0; // 消费者初始下标:从0开始取数据
product_step = 0; // 生产者初始下标:从0开始放数据
}
// 生产者接口:向环形队列放入数据(线程安全)
void PutData(const int &data)
{
// P操作(减1):申请可用空间
// 如果space_sem=0(无可用空间),生产者阻塞,直到消费者释放空间
sem_wait(&space_sem);
// 向当前生产者下标位置放入数据(环形队列的核心:下标直接覆盖,无需删除)
q[product_step] = data;
// 生产者下标后移:超过容量则取模,实现"环形复用"
product_step++;
product_step %= cap;
// V操作(加1):释放可用数据(通知消费者有新数据可消费)
sem_post(&data_sem);
}
// 消费者接口:从环形队列取出数据(线程安全)
void GetData(int &data)
{
// P操作(减1):申请可用数据
// 如果data_sem=0(无可用数据),消费者阻塞,直到生产者放入数据
sem_wait(&data_sem);
// 从当前消费者下标位置取出数据
data = q[consume_step];
// 消费者下标后移:超过容量则取模,实现"环形复用"
consume_step++;
consume_step %= cap;
// V操作(加1):释放可用空间(通知生产者有新空间可放数据)
sem_post(&space_sem);
}
// 析构函数:销毁信号量资源
~RingQueue()
{
sem_destroy(&data_sem); // 销毁数据信号量
sem_destroy(&space_sem); // 销毁空间信号量
}
};
// 消费者线程执行函数:循环从环形队列取数据
void *consumer(void *arg)
{
RingQueue *rqp = (RingQueue*)arg; // 接收环形队列对象指针
int data; // 存储取出的数据
for( ; ; ){ // 无限循环消费
rqp->GetData(data); // 从队列取数据(无数据时阻塞)
std::cout << "Consume data done : " << data << std::endl;
sleep(1); // 消费者休眠1秒,消费速度慢于生产者
}
}
// 生产者线程执行函数:循环向环形队列放数据(生产速度更快)
void *producter(void *arg)
{
RingQueue *rqp = (RingQueue*)arg; // 接收环形队列对象指针
srand((unsigned long)time(NULL)); // 初始化随机数种子
for( ; ; ){ // 无限循环生产
int data = rand() % 1024; // 生成0~1023的随机数作为生产数据
rqp->PutData(data); // 向队列放数据(无空间时阻塞)
std::cout << "Prodoct data done: " << data << std::endl;
// sleep(1); // 注释掉后生产者速度远快于消费者,会触发队列满的阻塞逻辑
}
}
int main()
{
RingQueue rq; // 创建环形队列对象(默认容量16)
pthread_t c,p; // 定义消费者、生产者线程ID
// 创建消费者线程:执行consumer函数,传入环形队列对象地址
pthread_create(&c, NULL, consumer, (void*)&rq);
// 创建生产者线程:执行producter函数,传入环形队列对象地址
pthread_create(&p, NULL, producter, (void*)&rq);
// 等待线程结束(线程是无限循环,实际不会执行到这)
// 作用:阻塞主线程,避免主线程提前销毁环形队列对象
pthread_join(c, NULL);
pthread_join(p, NULL);
return 0;
}
**12.**线程池
cpp
/*threadpool.h*/
/* 线程池:
* 一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着
监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利
用,还能防止过分调度。可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量。
* 线程池的应用场景:
* 1. 需要大量的线程来完成任务,且完成任务的时间比较短。 WEB服务器完成网页请求这样的任务,使用线程池技
术是非常合适的。因为单个任务小,而任务数量巨大,你可以想象一个热门网站的点击次数。 但对于长时间的任务,比如一个
Telnet连接请求,线程池的优点就不明显了。因为Telnet会话时间比线程的创建时间大多了。
* 2. 对性能要求苛刻的应用,比如要求服务器迅速响应客户请求。
* 3. 接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用。突发性大量客户请求,在没有线程池情
况下,将产生大量线程,虽然理论上大部分操作系统线程数目最大值不是问题,短时间内产生大量线程可能使内存到达极限,
出现错误.
* 线程池的种类:
* 线程池示例:
* 1. 创建固定数量线程池,循环从任务队列中获取任务对象,
* 2. 获取到任务对象后,执行任务对象中的任务接口
*/
/*threadpool.hpp*/
#ifndef __M_TP_H__ // 防止头文件重复包含
#define __M_TP_H__
#include <iostream>
#include <queue> // 任务队列底层容器
#include <pthread.h> // POSIX线程库
#define MAX_THREAD 5 // 线程池默认最大线程数
// 任务处理函数的函数指针类型:输入int,返回bool,表示任务执行结果
typedef bool (*handler_t)(int);
// 任务类:封装"任务数据"和"任务处理函数",实现任务的统一调度
class ThreadTask
{
private:
int _data; // 任务数据:比如请求的编号、待处理的数值等
handler_t _handler;// 任务处理函数指针:具体执行任务的逻辑由外部传入
public:
// 无参构造函数:初始化默认值
ThreadTask():_data(-1), _handler(NULL) {}
// 有参构造函数:初始化任务数据和处理函数
ThreadTask(int data, handler_t handler) {
_data= data;
_handler = handler;
}
// 设置任务(复用任务对象)
void SetTask(int data, handler_t handler) {
_data = data;
_handler = handler;
}
// 执行任务:调用处理函数处理任务数据
void Run() {
_handler(_data);
}
};
// 线程池类:管理固定数量的线程,维护任务队列,实现任务的异步执行
class ThreadPool
{
private:
int _thread_max; // 线程池最大线程数(固定值)
int _thread_cur; // 线程池当前存活的线程数
bool _tp_quit; // 线程池退出标记:true表示需要退出所有线程
std::queue<ThreadTask *> _task_queue; // 任务队列:存储待执行的任务对象
pthread_mutex_t _lock; // 互斥锁:保护任务队列的并发访问
pthread_cond_t _cond; // 条件变量:线程无任务时阻塞,有任务时唤醒
private:
// 私有方法:加锁(封装互斥锁操作,简化代码)
void LockQueue() {
pthread_mutex_lock(&_lock);
}
// 私有方法:解锁(封装互斥锁操作)
void UnLockQueue() {
pthread_mutex_unlock(&_lock);
}
// 私有方法:唤醒一个等待任务的线程(有新任务时调用)
void WakeUpOne() {
pthread_cond_signal(&_cond);
}
// 私有方法:唤醒所有等待任务的线程(线程池退出时调用)
void WakeUpAll() {
pthread_cond_broadcast(&_cond);
}
// 私有方法:线程退出处理(减少存活线程数,解锁并退出线程)
void ThreadQuit() {
_thread_cur--; // 存活线程数减1
UnLockQueue(); // 解锁:避免持有锁退出导致死锁
pthread_exit(NULL);// 线程退出
}
// 私有方法:线程等待(无任务时阻塞,或退出线程池)
void ThreadWait(){
// 如果线程池标记为退出,则当前线程直接退出
if (_tp_quit) {
ThreadQuit();
}
// 无任务时阻塞:释放锁,等待条件变量唤醒;唤醒后重新加锁
pthread_cond_wait(&_cond, &_lock);
}
// 私有方法:判断任务队列是否为空(仅在加锁后调用)
bool IsEmpty() {
return _task_queue.empty();
}
// 静态成员函数:线程入口函数(必须静态,因为pthread_create要求函数参数为void*)
// 作用:循环从任务队列取任务并执行
static void *thr_start(void *arg) {
// 将void*参数转为ThreadPool对象指针(arg是this指针)
ThreadPool *tp = (ThreadPool*)arg;
// 线程主循环:持续处理任务,直到线程池退出
while(1) {
tp->LockQueue(); // 加锁:保护任务队列
// 循环检查任务队列是否为空:避免虚假唤醒
while(tp->IsEmpty()) {
tp->ThreadWait(); // 无任务则等待(阻塞/退出)
}
// 取出任务队列头部的任务
ThreadTask *tt;
tp->PopTask(&tt);
tp->UnLockQueue(); // 解锁:任务执行无需持有锁(提升并发)
tt->Run(); // 执行任务逻辑
delete tt; // 释放任务对象内存(任务由外部new创建)
}
return NULL;
}
public:
// 构造函数:初始化线程池参数、互斥锁、条件变量
ThreadPool(int max=MAX_THREAD):_thread_max(max), _thread_cur(max), _tp_quit(false) {
pthread_mutex_init(&_lock, NULL); // 初始化互斥锁(默认属性)
pthread_cond_init(&_cond, NULL); // 初始化条件变量(默认属性)
}
// 析构函数:销毁互斥锁和条件变量
~ThreadPool() {
pthread_mutex_destroy(&_lock); // 销毁互斥锁
pthread_cond_destroy(&_cond); // 销毁条件变量
}
// 公有方法:初始化线程池(创建指定数量的工作线程)
bool PoolInit() {
pthread_t tid;
// 循环创建_max个工作线程
for (int i = 0; i < _thread_max; i++) {
// 创建线程:入口函数为thr_start,参数为ThreadPool对象的this指针
int ret = pthread_create(&tid, NULL, thr_start, this);
if (ret != 0) {
std::cout<<"create pool thread error\n";
return false;
}
}
return true;
}
// 公有方法:向任务队列提交任务(外部调用,线程安全)
bool PushTask(ThreadTask *tt) {
LockQueue(); // 加锁:保护任务队列
// 如果线程池已标记退出,拒绝接收新任务
if (_tp_quit) {
UnLockQueue();
return false;
}
_task_queue.push(tt); // 将任务放入队列
WakeUpOne(); // 唤醒一个等待的线程处理任务
UnLockQueue(); // 解锁
return true;
}
// 公有方法:从任务队列取出任务(仅在加锁后调用)
bool PopTask(ThreadTask **tt) {
*tt = _task_queue.front(); // 取出队列头部任务
_task_queue.pop(); // 删除队列头部任务
return true;
}
// 公有方法:退出线程池(唤醒所有线程,等待所有线程退出)
bool PoolQuit() {
LockQueue(); // 加锁:保护退出标记
_tp_quit = true; // 设置退出标记为true
UnLockQueue(); // 解锁
// 等待所有线程退出:循环唤醒+休眠,直到存活线程数为0
while(_thread_cur > 0) {
WakeUpAll(); // 唤醒所有阻塞的线程(让线程检测退出标记)
usleep(1000); // 休眠1ms,避免频繁唤醒消耗CPU
}
return true;
}
};
#endif
/*main.cpp*/
#include "threadpool.hpp" // 包含线程池头文件
#include <stdio.h>
#include <time.h>
#include <unistd.h>
// 任务处理函数:模拟具体的任务逻辑(比如业务处理、数据计算等)
bool handler(int data)
{
srand(time(NULL)); // 初始化随机数种子
int n = rand() % 5; // 生成0~4的随机数,模拟任务执行耗时
// 打印线程ID、任务数据、休眠时间
printf("Thread: %p Run Tast: %d--sleep %d sec\n", pthread_self(), data, n);
sleep(n); // 模拟任务执行耗时
return true; // 任务执行成功
}
int main()
{
int i;
ThreadPool pool; // 创建线程池对象(默认5个线程)
pool.PoolInit(); // 初始化线程池:创建5个工作线程
// 提交10个任务到线程池
for (i = 0; i < 10; i++) {
// 创建任务对象:任务数据为i,处理函数为handler
ThreadTask *tt = new ThreadTask(i, handler);
pool.PushTask(tt); // 提交任务到线程池
}
pool.PoolQuit(); // 退出线程池:等待所有线程处理完任务并退出
return 0;
}
**13.**线程安全的单例模式
单例模式是设计模式中创建型模式 的一种核心实现,它的核心目标是:保证一个类在整个程序生命周期中,只能创建出唯一一个实例对象,并且提供一个全局访问点来获取这个实例。
简单来说,你可以把单例模式理解为:某个类就像 "全局唯一的工具类",不管你在程序的哪个地方调用它的创建方法,拿到的都是同一个对象,不会出现多个实例并存的情况。
一、单例模式的核心特点
- 唯一性 :类的构造函数被私有化(
private),外部无法通过new直接创建实例; - 全局访问 :类内部提供一个静态方法(如
getInstance()),作为获取唯一实例的全局入口; - 延迟 / 饿汉创建:实例的创建时机可控制(饿汉式:程序启动就创建;懒汉式:第一次调用时创建)。
二、单例模式的两种经典实现(C++ 示例)
- 饿汉式单例(饿汉模式)
-
特点:程序启动时就创建实例("饿" 表示急于创建),线程安全(静态变量初始化由编译器保证原子性);
-
缺点:如果实例创建代价高,且程序全程未使用,会造成资源浪费。
#include <iostream>
using namespace std;class Singleton {
private:
// 1. 私有化构造函数:外部无法new
Singleton() {
cout << "饿汉式单例:创建实例" << endl;
}// 2. 私有化拷贝构造和赋值运算符:禁止拷贝 Singleton(const Singleton&) = delete; Singleton& operator=(const Singleton&) = delete; // 3. 静态成员变量:程序启动时初始化(唯一实例) static Singleton* instance;public:
// 4. 全局访问点:获取唯一实例
static Singleton* getInstance() {
return instance;
}// 测试方法:验证实例唯一性 void show() { cout << "当前实例地址:" << this << endl; }};
// 静态成员变量初始化(程序启动时执行)
Singleton* Singleton::instance = new Singleton();// 测试
int main() {
// 多次获取实例,地址完全相同
Singleton* s1 = Singleton::getInstance();
Singleton* s2 = Singleton::getInstance();s1->show(); // 输出:当前实例地址:0xXXXXXXX s2->show(); // 输出:当前实例地址:0xXXXXXXX(和s1一致) return 0;}
- 懒汉式单例(懒汉模式)
-
特点 :第一次调用
getInstance()时才创建实例("懒" 表示延迟创建),节省资源; -
注意:多线程环境下需加锁,避免 "竞态条件" 导致创建多个实例。
#include <iostream>
#include <pthread.h>
using namespace std;class Singleton {
private:
// 1. 私有化构造函数
Singleton() {
cout << "懒汉式单例:创建实例" << endl;
}// 2. 禁止拷贝 Singleton(const Singleton&) = delete; Singleton& operator=(const Singleton&) = delete; // 3. 静态成员变量:初始为NULL static Singleton* instance; // 互斥锁:保证多线程安全 static pthread_mutex_t lock;public:
// 4. 全局访问点:加锁保证线程安全
static Singleton* getInstance() {
// 双重检查锁(DCL):减少锁竞争,提升性能
if (instance == NULL) { // 第一次检查:避免每次调用都加锁
pthread_mutex_lock(&lock); // 加锁
if (instance == NULL) { // 第二次检查:防止多线程同时进入第一层判断
instance = new Singleton();
}
pthread_mutex_unlock(&lock); // 解锁
}
return instance;
}void show() { cout << "当前实例地址:" << this << endl; }};
// 静态成员变量初始化
Singleton* Singleton::instance = NULL;
pthread_mutex_t Singleton::lock = PTHREAD_MUTEX_INITIALIZER;// 测试
int main() {
Singleton* s1 = Singleton::getInstance(); // 第一次调用:创建实例
Singleton* s2 = Singleton::getInstance(); // 后续调用:直接返回已有实例s1->show(); // 地址相同 s2->show(); // 地址相同 return 0;}
三、单例模式的适用场景
- 资源独占的场景:比如数据库连接池、线程池、日志管理器、配置文件管理器 ------ 这些组件只需一个实例,避免重复创建导致资源浪费或状态混乱;
- 全局状态管理:比如程序的全局配置类,确保所有模块读取的是同一个配置;
- 性能敏感场景:创建实例代价高(如初始化耗时、占用内存大),单例模式可避免重复创建。
四、单例模式的注意事项
- 线程安全 :懒汉式必须加锁(如上述的互斥锁),否则多线程并发调用
getInstance()可能创建多个实例; - 内存泄漏:C++ 中需注意单例实例的释放(可通过析构函数、atexit 注册释放函数,或使用局部静态变量的懒汉式自动释放);
- 避免滥用:如果类不需要全局唯一,强行使用单例会增加耦合度,降低代码可测试性。
**14. STL,**智能指针和线程安全
STL中的容器是否是线程安全的?
不是.
原因是, STL 的设计初衷是将性能挖掘到极致, 而一旦涉及到加锁保证线程安全, 会对性能造成巨大的影响.而且对于不同的容器, 加锁方式的不同, 性能可能也不同(例如hash表的锁表和锁桶).
因此 STL 默认不是线程安全. 如果需要在多线程环境下使用, 往往需要调用者自行保证线程安全
智能指针是否是线程安全的**?**
对于 unique_ptr, 由于只是在当前代码块范围内生效, 因此不涉及线程安全问题.
对于 shared_ptr, 多个对象需要共用一个引用计数变量, 所以会存在线程安全问题. 但是标准库实现的时候考虑到了这个问题, 基于原子操作(CAS)的方式保证 shared_ptr 能够高效, 原子的操作引用计数
**15.**其他常见的各种锁
悲观锁:在每次取数据时,总是担心数据会被其他线程修改,所以会在取数据前先加锁(读锁,写锁,行锁等),当其他线程想要访问数据时,被阻塞挂起。
乐观锁:每次取数据时候,总是乐观的认为数据不会被其他线程修改,因此不上锁。但是在更新数据前,会判断其他数据在更新前有没有对数据进行修改。主要采用两种方式:版本号机制和CAS操作。CAS操作:当需要更新数据时,判断当前内存值和之前取得的值是否相等。如果相等则用新值更新。若不等则失败,失败则重试,一般是一个自旋的过程,即不断重试。
自旋锁,公平锁,非公平锁?