C语言可重入函数如何实现与注意事项?

99ANYc3cd6
预计阅读时长 18 分钟
位置: 首页 C语言 正文

什么是可重入函数?

可重入函数 指的是一个函数,它可以被多个任务(或线程)并发调用,并且在任何时候中断,再次进入该函数执行,其结果都是正确和可预测的。

c语言 reentrant
(图片来源网络,侵删)

一个可重入函数在执行过程中,可以被“打断”,安全地”再次进入,而不会破坏自身的数据或导致逻辑错误。

核心思想:函数的执行不依赖于任何“持久性”的状态。


为什么需要可重入函数?

可重入函数主要为了解决 并发中断 带来的问题。

想象一个场景:一个函数正在处理一个全局变量,此时发生了中断(或另一个线程抢占了CPU),中断服务程序也调用了同一个函数。

c语言 reentrant
(图片来源网络,侵删)
  • 非可重入函数:如果这个函数依赖全局变量,那么中断服务程序可能会修改这个全局变量,导致函数返回后,原始计算结果出错,这就是 “重入” 导致的 “数据竞争”
  • 可重入函数:它不依赖全局变量,所有数据都通过参数传入,结果通过返回值或指针参数传出,无论被调用多少次,都不会互相干扰。

主要应用场景:

  1. 多线程编程:多个线程同时调用同一个函数。
  2. 中断服务程序:在主程序执行时,一个中断发生了,ISR 也调用了这个函数。
  3. 信号处理:在程序执行时,收到了一个信号,信号处理函数也调用了这个函数。

可重入函数的关键特性

一个函数要成为可重入函数,必须满足以下条件:

  1. 不使用任何静态或全局数据:函数内部不能访问或修改全局变量、静态局部变量等,因为这些数据是共享的,在并发环境下会被多个执行流修改,导致数据不一致。
  2. 不返回任何指向静态或全局数据的指针:函数不能返回一个指向全局数据或静态数据的地址,因为调用者可能会在函数返回后修改这个数据,从而影响到其他正在使用这个数据的调用。
  3. 只使用局部数据:所有需要的数据都必须通过函数参数传入,或者作为函数内部的局部变量创建,局部变量存储在 上,每个函数调用都有自己独立的栈空间,因此是线程安全的。
  4. 不调用任何不可重入的函数:如果一个函数内部调用了 mallocprintffopen 等标准库函数,而它自己又依赖于这些函数的内部状态(malloc 的内存池),那么它本身也就不可重入了。

可重入 vs. 线程安全

这是一个非常容易混淆的概念,但它们有明确的区别。

特性 可重入 线程安全
核心关注点 函数能否被中断后再次安全进入。 函数能否被多个线程并发调用。
依赖关系 线程安全的函数不一定是可重入的。 可重入的函数一定是线程安全的。
实现方式 不依赖任何静态/全局状态,只使用局部变量和参数。 可以使用同步机制(如互斥锁、信号量)来保护共享数据。
中断安全性 必须是中断安全的。 不一定,一个线程安全的函数如果在执行时被中断,ISR 调用它可能会导致死锁(如果锁了锁)或数据竞争。

举个例子来理解它们的区别:

#include <stdio.h>
#include <string.h>
// 1. 不可重入,也不是线程安全的
void unsafe_function() {
    static int counter = 0; // 静态变量,共享状态
    counter++;
    printf("Counter is now: %d\n", counter);
}
// 2. 可重入,也是线程安全的
int reentrant_function(int a, int b) {
    int result = a + b; // 只使用局部变量
    return result;
}
// 3. 线程安全,但不可重入
#include <pthread.h>
pthread_mutex_t my_mutex = PTHREAD_MUTEX_INITIALIZER;
int shared_data = 0;
void thread_safe_but_not_reentrant() {
    pthread_mutex_lock(&my_mutex); // 加锁
    shared_data++; // 修改共享数据
    printf("Shared data is now: %d\n", shared_data);
    pthread_mutex_unlock(&my_mutex); // 解锁
}

分析 thread_safe_but_not_reentrant 为什么不可重入:

假设主线程正在执行这个函数,它成功获取了锁 my_mutex,一个中断发生了,中断服务程序也调用了 thread_safe_but_not_reentrant,当 ISR 试图获取同一个锁时,它会因为锁已经被主线程持有而阻塞,主线程正在被 ISR 阻塞,无法继续执行以解锁,这就造成了 “死锁”,这个函数在ISR环境下是不安全的,即不可重入。


如何编写可重入函数?(最佳实践)

  1. 避免使用全局变量和静态变量:这是最重要的一条,所有数据都应通过函数参数传递。
  2. 使用局部变量:将所有临时数据声明为函数内部的局部变量。
  3. 谨慎使用标准库函数:并非所有标准库函数都是可重入的。
    • 不可重入malloc, free, printf, scanf, strtok, rand, asctime, getenv 等,它们内部通常使用静态缓冲区。
    • 可重入版本:C标准库提供了一组带 _r 后缀的可重入版本,malloc_r, printf_r, strtok_r 等,在嵌入式或对实时性要求高的环境中,应优先使用这些版本。
  4. 如果必须访问共享资源,使用同步机制:在函数内部,如果需要访问一个全局资源(比如硬件寄存器),应该使用互斥锁来保护,但请记住,这样做会让函数变得不可重入(如上例所示),在这种情况下,你需要明确这个函数是否需要在ISR中调用,并做出权衡。

经典示例:strtok vs. strtok_r

这是一个展示可重入重要性的绝佳例子。

strtok - 不可重入

strtok 的功能是分割字符串,它的内部实现使用了一个静态指针来记住当前分割的位置。

#include <stdio.h>
#include <string.h>
void print_tokens(const char* str) {
    char* token;
    // 第一次调用,strstr不能是NULL
    token = strtok((char*)str, " ");
    while (token != NULL) {
        printf("Token: %s\n", token);
        // 后续调用,strstr必须是NULL,以继续上一次的分割
        token = strtok(NULL, " ");
    }
}
int main() {
    char str1[] = "hello world";
    char str2[] = "foo bar";
    // 在多线程环境下,这会导致严重问题
    // 即使是单线程,下面这个例子也说明了问题
    print_tokens(str1); // 输出 "hello", "world"
    print_tokens(str2); // 输出 "foo", "bar"
    // 但如果在第一次调用未完成时发生中断,ISR也调用了strtok,
    // 那么ISR的调用会覆盖掉主调用的静态指针,导致主调用的逻辑错误。
    return 0;
}

strtok 依赖一个静态的、隐藏的指针来记录分割进度,如果两个线程同时调用 strtok,它们会互相干扰这个静态指针,导致分割结果混乱。strtok不可重入 的。

strtok_r - 可重入

strtok_rstrtok 的可重入版本,它通过一个额外的 char** saveptr 参数,让调用者自己来保存分割状态。

#include <stdio.h>
#include <string.h>
void print_tokens_r(const char* str, char* saveptr) {
    char* token;
    token = strtok_r((char*)str, " ", &saveptr);
    while (token != NULL) {
        printf("Token: %s\n", token);
        token = strtok_r(NULL, " ", &saveptr);
    }
}
int main() {
    char str1[] = "hello world";
    char str2[] = "foo bar";
    char* saveptr1; // 用于保存str1的分割状态
    char* saveptr2; // 用于保存str2的分割状态
    print_tokens_r(str1, &saveptr1); // 输出 "hello", "world"
    print_tokens_r(str2, &saveptr2); // 输出 "foo", "bar"
    // 即使在多线程环境下,只要每个线程有自己的saveptr变量,
    // 它们就不会互相干扰。
    return 0;
}

strtok_r 将原本函数内部的“状态”(分割位置)交给了调用者来管理(通过 saveptr 指针),每个调用者都拥有自己独立的 saveptr,因此可以安全地并发执行。strtok_r可重入 的。


概念 核心思想 关键点
可重入函数 函数可以被中断,然后再次安全进入,结果正确。 不依赖任何持久性(静态/全局)状态,只使用局部变量和参数。
线程安全函数 函数可以被多个线程并发调用,结果正确。 可以通过锁等同步机制保护共享数据。
关系 可重入 => 线程安全,但反之不成立。 线程安全的函数在ISR中可能死锁,因此不可重入。

在编写高性能、高可靠性的 C 代码,尤其是在操作系统、驱动、网络库等底层模块时,始终将函数设计为可重入的 是一个非常好的编程习惯,它能极大地减少并发相关的 Bug,提高代码的健壮性和可维护性。

-- 展开阅读全文 --
头像
timestamp在C语言中如何使用?
« 上一篇 今天
c语言httpclient
下一篇 » 今天

相关文章

取消
微信二维码
支付宝二维码

目录[+]