详解可重入函数与不可重入函数

详解可重入函数与不可重入函数,第1张

C/C++ 中的可重入函数与不可重入函数 引言

在编程语言发展的历程中,“函数”的概念无疑是最历史中最伟大的发明之一。


简单来说,函数是完成指定功能的代码集合。


函数的基本样式:

int func1 (int a, int b)
{
    int add1 = a;
    int add2 = b;
    int sum = a + b;
    
    printf("add1 addr=%p, add2 addr=%p, sum addr=%p", &add1, &add2, &sum);
    return sum;
}

我们开发函数的意义除希望它完成指定的功能外,还在于它可以被很多模块调用,而不必重复开发。


如上述执行加法运算的函数,它当然可以被很多模块调用,然后返回传入的两个值 a 和 b 相加的和。


然而,在我们期望"一个函数"可以满足多个模块的使用需求时,我们还需要考虑当多个模块同时调用同一个函数时的"竞争"问题。


这编引出了今天讨论的内容:函数的可重入性和不可重入性。


“重入”的概念

重入(re-enter),是指当一个函数 func() 已经被 模块 A 调用时,还可以同时被其他模块B、C调用,并且保证 A、B、C 三个模块都能通过调用函数func() 获取正确的结果。


用人话说就是这个函数可以同时被很多模块调用,并且保证每个模块都能得到期望的结果。


就问 6 不 6?

可重入函数就是,可以被多个模块(如任务、线程、进程)同时使用,且能保证每个模块结果都正常的函数;由于这种可以被多个模块使用的特性,可重入函数也被称为(多)线程安全函数、(多)进程安全函数、可并发函数。


"可重入"与“不可重入” 的示例

示例:

//可重入函数
int func1 (char* tag, int a, int b)
{
    int add1 = a;
    int add2 = b;
    int sum = a + b;
    
    printf("%s func1: : add1 addr=%p, add2 addr=%p, sum addr=%p sum1=%d\r\n",tag, &add1, &add2, &sum, sum);
    return sum;
}
// 不可重入函数
static int global_count;
int func2 (char *tag, int a, int b)
{
    int add1 = a;
    int add2 = b;
    // add mux
    int sum = a + b + global_count;
    global_count++;
    
    printf("%s func2: add1 addr=%p, add2 addr=%p, sum addr=%p, global addr=%p, sum2=%d\r\n",tag, &add1, &add2, &sum, &global_count, sum);
    // remove mux
    return sum;
}

static void task1_process(void *arg)
{
    char *TASK1_TAG = "TASK1";
    int sub1 = 1, sub2 = 2;
    while (1) {
        func1(TASK1_TAG, sub1, sub2);
        // func2(TASK1_TAG, sub1, sub2);
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

static void task2_process(void *arg)
{
    char *TASK2_TAG = "TASK2";
    int sub1 = 1, sub2 = 2;
    while (1) {
        func1(TASK2_TAG, sub1, sub2);
        // func2(TASK2_TAG, sub1, sub2);
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

void app_main(void)
{
    printf("Hello world!\n");

    xTaskCreate(&task1_process, "task1", 1024*2, (void *)"1", 2, NULL);
    xTaskCreate(&task2_process, "task2", 1024*2, (void *)"2", 3, NULL);
}
可重入函数 func1 分析

当 两个线程(任务)都调用 func1() 时,打印的结果如下所示,task1、task2 得到的结果 sum 都是正确的。


特别地,在 task1、task2 中 函数 func1()内的变量 add1、add2、sum 的地址都是不同的。


这意味着,在 task1 中的 func1() 使用的是 task1 对应的内存空间,在 task2 中的 func1() 使用的是 task2 对应的内存空间。


Hello world!
TASK2 func1: : add1 addr=0x3ffb7cb0, add2 addr=0x3ffb7cb4, sum addr=0x3ffb7cb8 sum1=3
TASK1 func1: : add1 addr=0x3ffb74b0, add2 addr=0x3ffb74b4, sum addr=0x3ffb74b8 sum1=3
TASK2 func1: : add1 addr=0x3ffb7cb0, add2 addr=0x3ffb7cb4, sum addr=0x3ffb7cb8 sum1=3
TASK1 func1: : add1 addr=0x3ffb74b0, add2 addr=0x3ffb74b4, sum addr=0x3ffb74b8 sum1=3

我们可以得到这样的结论: 两个 task(也可称为两个模块、线程)虽然调用的是同一个函数,但函数内的变量使用的是相对独立的地址来存储数据,因此数据之间互不干扰。


不可重入函数 func2 分析

当 两个线程(任务)都调用 func2() 时,打印的结果如下所示,task1、task2 得到的结果 sum 出现混乱,task2 的 结果,理应是 4、5、6 这样以1 递增,但在第三次中得到的结果是7 。


特别地,在 task1、task2 中 函数 func2()内的变量 add1、add2、sum 的地址都是不同的,但函数内的变量 global_count 的地址是固定的 0x3ffb2a70 。


这意味着,在 task1 中的 func2() 使用的变量 global_count 与在 task2 中的 func2() 使用的变量 global_count 是 同一个。


Hello world!
TASK2 func2: add1 addr=0x3ffb7cc0, add2 addr=0x3ffb7cc4, sum addr=0x3ffb7cc8, global addr=0x3ffb2a70, sum2=4
TASK1 func2: add1 addr=0x3ffb74b0, add2 addr=0x3ffb74b4, sum addr=0x3ffb74b8, global addr=0x3ffb2a70, sum2=3
TASK2 func2: add1 addr=0x3ffb7cc0, add2 addr=0x3ffb7cc4, sum addr=0x3ffb7cc8, global addr=0x3ffb2a70, sum2=5
TASK1 func2: add1 addr=0x3ffb74b0, add2 addr=0x3ffb74b4, sum addr=0x3ffb74b8, global addr=0x3ffb2a70, sum2=6
TASK2 func2: add1 addr=0x3ffb7cc0, add2 addr=0x3ffb7cc4, sum addr=0x3ffb7cc8, global addr=0x3ffb2a70, sum2=7
TASK1 func2: add1 addr=0x3ffb74b0, add2 addr=0x3ffb74b4, sum addr=0x3ffb74b8, global addr=0x3ffb2a70, sum2=8

我们可以得到这样的结论: 两个 task(也可称为两个模块、线程)虽然调用的是同一个函数,但函数内若存在共享的变量(如上述的 global_count),则该函数内的数据之间可能发生干扰,导致出现不可预知的错误。


可重入与不可重入的底层原因

为什么上述函数 func1 是可重入的,而 func2 是不可重入的?

我们通过打印函数内的地址,发现可重入的函数内的变量使用的是对应模块的相对独立的地址来存储数据;

可重入的原理如下图所示,三个模块都调用了 func,但 func 分别使用对应三个模块的内存空间,它们各玩各,互不干扰,嘿嘿。




不可重入的函数内部使用了共享的一段空间来存储数据,当三个模块同时访问 func 时,将间接地同时共用一个变量 global_count,导致数据之间的干扰。



如何写出可重入的函数

一句话概况:要么仅使用本地变量,要么在使用共享资源时添加保护,来避免共享资源之间的干扰。


具体地,在函数中要注意下述行为:

1)函数内减少使用全局变量,以及静态数据,若必须使用,请添加保护。


2)函数内减少使用malloc()\free()、以及标准I\O 的函数(其一些子函数不可重入,如 printf()),若必须使用,请添加保护。


3)函数内对设备硬件资源,,如 UART 的访问,要注意关闭中断,使用后再重新开启中断。


4)函数内不调用其他不可重入的子函数。


5)注意评估模块的堆栈空间,谨防堆栈不够大,导致的堆栈溢出。


6)函数中不要使用浮点运算(这个与处理器相关,但部分处理器的浮点运算都是不可重入的)。


(预告:使用互斥锁,对共享资源进行保护)
(码字不易,谢谢点赞及收藏)

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/langs/578039.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-04-11
下一篇 2022-04-11

发表评论

登录后才能评论

评论列表(0条)

保存