函数的可重入性决定了函数的线程安全?volatile被认为是可重入关键字?

一、核心背景

可重入 / 不可重入、线程安全 / 不安全,都是 **「并发执行场景」**下的函数特性,这个场景包含两种核心情况:

  1. 单线程中:函数执行到一半,被信号 / 中断打断,转而执行这个函数本身,执行完再回到断点继续;
  2. 多线程中:多个线程并发调用同一个函数
  3. 特殊场景:函数递归调用自身

二、可重入函数 & 不可重入函数

可重入函数

一个函数被调用执行时,在执行流程被任意打断后 (比如中断、信号、线程切换),再次被调用 / 再次进入执行 ,当后续恢复断点继续执行时,最终的执行结果依然正确、逻辑无错乱、数据无异常

简单说:函数「被打断后再进入」,结果不受任何影响,怎么进都安全

不可重入函数

和可重入函数完全相反:一个函数被调用执行时,如果执行流程被打断并再次进入执行 ,恢复断点后,会出现 逻辑错乱、数据计算错误、返回值异常、甚至程序崩溃 的情况。

简单说:函数「不能被打断后再进入」,只能从头执行到尾,否则必出问题

核心本质区别

可重入函数:执行过程中「不依赖、不修改任何共享资源」,只使用「自身栈内的局部数据」和「调用者传入的参数」

不可重入函数:执行过程中「依赖或修改了共享资源」,或「调用了其他不可重入函数」,这是所有不可重入的根源!

共享资源包含哪些?

所有「不属于当前函数私有、被多段执行逻辑共用」的资源,都是共享资源,主要包括:

  1. 全局变量、静态全局变量、静态局部变量;
  2. 堆内存中的共享数据(比如全局的链表、哈希表);
  3. 硬件寄存器、外设设备等系统级共享资源;
  4. 标准 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 线程不安全函数 关系与本质

结论:

  • 所有「不可重入函数」,一定是「线程不安全函数」 (单向必然成立,无例外)
  • 所有「线程不安全函数」,不一定是「不可重入函数」 (反向不成立,存在例外)
  • 关系总结:不可重入 是 线程不安全的「充分非必要条件」
为什么「不可重入函数 → 必然线程不安全」?
  1. 不可重入函数的核心缺陷:依赖 / 修改「共享资源」(全局 / 静态变量等),且对共享资源的访问「无任何互斥保护机制」
  2. 多线程的核心特征:多个线程并发执行,共享进程的全局 / 静态资源,线程切换是随机的

当多线程调用一个不可重入函数时,本质上就是「多个线程在随机的时间点,并发操作同一个共享资源」,必然会发生 「竞态条件 」 ------ 比如一个线程刚读了共享变量的值,还没来得及修改,就被切换走,另一个线程修改了这个变量,原线程恢复后基于旧值修改,最终数据必然错误。

本质相通 :不可重入函数的「被中断重入」 和 线程不安全的「多线程并发调用」,本质都是**「并发访问无保护的共享资源」**,只是并发的触发方式不同(中断触发 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:函数本身可重入,但「调用了线程不安全的外部接口 / 资源」

比如一个可重入的计算函数,内部调用了未加锁的共享数据库连接、未加锁的文件句柄,此时函数本身是可重入的,但因为依赖了线程不安全的外部资源,最终这个函数也变成了线程不安全的函数。

四、如何编写「可重入函数」?

满足以下条件即可写出绝对的可重入函数,也是写出线程安全函数的基础:

  1. 不使用全局变量、静态全局变量、静态局部变量
  2. 不修改传入的指针参数指向的共享内存
  3. 只使用函数形参、栈上的局部变量
  4. 函数内部只调用其他可重入函数
  5. 不依赖任何共享的硬件资源 / 系统资源

五、线程安全函数的两种实现方式

线程安全是比可重入「更高维度」的安全,线程安全函数包含两类:

  1. 第一类:本身就是可重入函数 → 天然线程安全(无共享资源,无需任何保护);
  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;
}

结论:

  1. volatile 只是保证变量的「可见性」,解决「编译器优化导致的脏读」;
  2. volatile 不解决「并发修改的竞态条件」,这个函数依然因为修改全局变量,是不可重入的,多线程调用依然线程不安全;
  3. volatile 是「写可重入函数的好帮手」,但绝对不是「可重入关键字」。
相关推荐
寻星探路9 小时前
【Python 全栈测开之路】Python 基础语法精讲(三):函数、容器类型与文件处理
java·开发语言·c++·人工智能·python·ai·c#
无限进步_9 小时前
【C语言&数据结构】相同的树:深入理解二叉树的结构与值比较
c语言·开发语言·数据结构·c++·算法·github·visual studio
枫叶丹49 小时前
【Qt开发】Qt系统(五)-> Qt 多线程
c语言·开发语言·c++·qt
Larry_Yanan9 小时前
Qt多进程(九)命名管道FIFO
开发语言·c++·qt·学习·ui
聆风吟º9 小时前
【C++藏宝阁】C++入门:命名空间(namespace)详解
开发语言·c++·namespace·命名空间
优雅的潮叭9 小时前
c++ 学习笔记之 模板元编程
c++·笔记·学习
飞鹰519 小时前
CUDA入门:从Hello World到矩阵运算 - Week 1学习总结
c++·人工智能·性能优化·ai编程·gpu算力
CSDN_RTKLIB9 小时前
【std::vector】vector<T*>与vector<T>*
c++·stl
fpcc9 小时前
跟我学C++中级篇——对类const关键字的分析说明
c++