一、核心背景
可重入 / 不可重入、线程安全 / 不安全,都是 **「并发执行场景」**下的函数特性,这个场景包含两种核心情况:
- 单线程中:函数执行到一半,被信号 / 中断打断,转而执行这个函数本身,执行完再回到断点继续;
- 多线程中:多个线程并发调用同一个函数;
- 特殊场景:函数递归调用自身。
二、可重入函数 & 不可重入函数
可重入函数
一个函数被调用执行时,在执行流程被任意打断后 (比如中断、信号、线程切换),再次被调用 / 再次进入执行 ,当后续恢复断点继续执行时,最终的执行结果依然正确、逻辑无错乱、数据无异常。
简单说:函数「被打断后再进入」,结果不受任何影响,怎么进都安全。
不可重入函数
和可重入函数完全相反:一个函数被调用执行时,如果执行流程被打断并再次进入执行 ,恢复断点后,会出现 逻辑错乱、数据计算错误、返回值异常、甚至程序崩溃 的情况。
简单说:函数「不能被打断后再进入」,只能从头执行到尾,否则必出问题。
核心本质区别
可重入函数:执行过程中「不依赖、不修改任何共享资源」,只使用「自身栈内的局部数据」和「调用者传入的参数」;
不可重入函数:执行过程中「依赖或修改了共享资源」,或「调用了其他不可重入函数」,这是所有不可重入的根源!
共享资源包含哪些?
所有「不属于当前函数私有、被多段执行逻辑共用」的资源,都是共享资源,主要包括:
- 全局变量、静态全局变量、静态局部变量;
- 堆内存中的共享数据(比如全局的链表、哈希表);
- 硬件寄存器、外设设备等系统级共享资源;
- 标准 IO 缓冲区(比如
printf的输出缓冲区)。
补充:函数的局部变量 / 形参 是存储在栈 (stack) 中的,栈是「线程私有、函数调用私有」的,不同的函数调用 / 线程调用,栈帧完全独立,所以这类数据是绝对安全的,不会导致不可重入。
可重入 / 不可重入 详细对比表:
|----------|-----------------------|--------------------------|
| 特性维度 | 可重入函数 | 不可重入函数 |
| 数据依赖 | 仅依赖局部变量、函数形参(栈私有) | 依赖 / 修改全局变量、静态变量(共享) |
| 资源修改 | 不修改任何共享资源 | 会修改共享资源 |
| 函数调用 | 仅调用「可重入函数」 | 调用了「不可重入函数」(如strtok) |
| 执行结果 | 被打断重入后,结果依然正确 | 被打断重入后,结果必然错误 / 紊乱 |
| 执行安全 | 任意场景下调用都安全 | 并发 / 中断场景下调用必出问题 |
| 设计原则 | 无状态、纯逻辑计算 | 有状态、依赖外部数据状态 |
经典代码案例
案例 1:可重入函数 示例(安全)
cpp
// 纯计算函数,只使用形参和局部变量,无任何共享资源
int add(int a, int b) {
int temp = a + 1; // 局部变量(栈私有)
return temp + b;
}
// 字符串拷贝,只使用形参,不修改全局/静态资源
void my_strcpy(char *dest, const char *src) {
while(*src != '\0') {
*dest++ = *src++;
}
*dest = '\0';
}
特点:无论被中断多少次、多线程怎么调用,结果永远正确,因为所有数据都是「自己的」,无共享。
案例 2:不可重入函数 示例(不安全)
cpp
// 问题1:依赖并修改全局变量
int g_count = 0;
int count_add(int a) {
g_count += a; // 修改全局共享变量
return g_count;
}
// 问题2:依赖并修改静态局部变量(C语言经典反例)
char* my_strtok(char *str, const char *delim) {
static char *save_ptr; // 静态局部变量,函数调用间共享状态
if (str == NULL) {
str = save_ptr;
}
// ... 分割逻辑 ...
save_ptr = str; // 修改静态变量
return str;
}
问题根源:比如count_add,线程 1 执行到g_count += a一半时被切换,线程 2 也调用count_add修改了g_count,线程 1 恢复后,g_count的值已经被篡改,返回结果必然错误。
三、不可重入函数 VS 线程不安全函数 关系与本质
结论:
- 所有「不可重入函数」,一定是「线程不安全函数」 (单向必然成立,无例外)
- 所有「线程不安全函数」,不一定是「不可重入函数」 (反向不成立,存在例外)
- 关系总结:不可重入 是 线程不安全的「充分非必要条件」
为什么「不可重入函数 → 必然线程不安全」?
- 不可重入函数的核心缺陷:依赖 / 修改「共享资源」(全局 / 静态变量等),且对共享资源的访问「无任何互斥保护机制」;
- 多线程的核心特征:多个线程并发执行,共享进程的全局 / 静态资源,线程切换是随机的;
当多线程调用一个不可重入函数时,本质上就是「多个线程在随机的时间点,并发操作同一个共享资源」,必然会发生 「竞态条件 」 ------ 比如一个线程刚读了共享变量的值,还没来得及修改,就被切换走,另一个线程修改了这个变量,原线程恢复后基于旧值修改,最终数据必然错误。
本质相通 :不可重入函数的「被中断重入」 和 线程不安全的「多线程并发调用」,本质都是**「并发访问无保护的共享资源」**,只是并发的触发方式不同(中断触发 vs 线程调度触发),最终的错误结果是完全一样
一句话概括:不可重入的函数,天生就不具备在多线程环境下安全执行的能力。
为什么「线程不安全函数 → 不一定是不可重入函数」?
核心原因是:线程不安全的「成因更多」,不可重入只是其中一种成因。
一个函数本身可以是「可重入」的 (满足可重入的所有条件:只使用局部变量、形参,不依赖共享资源),但依然可能是线程不安全的,这类情况就是「线程不安全的非不可重入函数」,常见 2 种场景:
场景 1:函数本身可重入,但「操作了线程共享的堆内存」且无保护
cpp
// 本函数是【可重入】的:无全局/静态变量,只用局部变量和形参
void insert_node(Node *new_node, List *global_list) {
// 逻辑:把新节点插入链表头部
new_node->next = global_list->head;
global_list->head = new_node;
}
- 可重入性:函数本身没有任何共享资源,被中断重入后执行结果依然正确,是可重入函数;
- 线程安全性:
global_list是堆内存中的全局链表 ,是线程共享资源,多线程调用insert_node时,并发修改链表的head指针,必然出现链表断裂、节点丢失等问题,是线程不安全函数。
场景 2:函数本身可重入,但「调用了线程不安全的外部接口 / 资源」
比如一个可重入的计算函数,内部调用了未加锁的共享数据库连接、未加锁的文件句柄,此时函数本身是可重入的,但因为依赖了线程不安全的外部资源,最终这个函数也变成了线程不安全的函数。
四、如何编写「可重入函数」?
满足以下条件即可写出绝对的可重入函数,也是写出线程安全函数的基础:
- 不使用全局变量、静态全局变量、静态局部变量;
- 不修改传入的指针参数指向的共享内存;
- 只使用函数形参、栈上的局部变量;
- 函数内部只调用其他可重入函数;
- 不依赖任何共享的硬件资源 / 系统资源。
五、线程安全函数的两种实现方式
线程安全是比可重入「更高维度」的安全,线程安全函数包含两类:
- 第一类:本身就是可重入函数 → 天然线程安全(无共享资源,无需任何保护);
- 第二类:本身是不可重入函数 ,但通过加锁(互斥锁
mutex、自旋锁)对共享资源做了「互斥访问保护」→ 变成线程安全函数。
例子:
malloc()本身是不可重入的,但现代 C 库中malloc()内部加了锁,所以多线程调用malloc()是安全的。
六、volatile 关键字(99% 的人把它当「可重入关键字」)
地位:可重入编程的「必备辅助关键字」,不是「可重入关键字」
volatile 的核心作用(和可重入强相关)
volatile 直译是「易变的」,它告诉编译器:被修饰的变量,值可能会被「当前程序之外的因素」意外修改 (比如硬件中断、其他线程、内核态程序),编译器禁止对这个变量做任何优化,必须「每次读写都直接访问内存」,而不是把变量缓存到 CPU 寄存器中。
为什么它和可重入绑定在一起?
不可重入的核心风险之一,就是「共享变量(全局 / 静态)被意外篡改」,而 volatile 刚好能解决编译器优化导致的「变量值不一致」问题 ,是实现「正确的可重入逻辑」的必要条件 。比如:单线程中函数被中断打断、多线程并发时,被 volatile 修饰的共享变量,能保证读到的永远是「内存中最新的真实值」,不会因为编译器优化读到寄存器的旧值,导致逻辑错乱。
volatile ≠ 可重入
cpp
// 例子:加了volatile,依然是【不可重入函数】
volatile int g_num = 0; // 加了volatile修饰全局变量
int add_volatile(int a) {
g_num += a;
return g_num;
}
结论:
volatile只是保证变量的「可见性」,解决「编译器优化导致的脏读」;volatile不解决「并发修改的竞态条件」,这个函数依然因为修改全局变量,是不可重入的,多线程调用依然线程不安全;volatile是「写可重入函数的好帮手」,但绝对不是「可重入关键字」。