可冲入函数与多线程安全函数

本文探讨了reentrant与thread-safe函数的区别,包括它们的概念、典型例子及如何确保函数在这两种场景下正确运行。强调了reentrant函数在信号处理和递归调用中的重要性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1. reentrant函数

一个函数是reentrant的,如果它可以被安全地递归或并行调用。要想成为reentrant式的函数,该函数不能含有(或使用)静态(或全局)数据(来存储函数调用过程中的状态信息),也不能返回指向静态数据的指针,它只能使用由调用者提供的数据,当然也不能调用non-reentrant函数.

比较典型的non-reentrant函数有getpwnam, strtok, malloc等.

reentrant和non-reentrant函数的例子

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <signal.h>
  4. #include <string.h>
  5. #include <sys/types.h>
  6. #include <unistd.h>
  7. #include <math.h>
  8. int* getPower(int i)
  9. {
  10.   static int result;
  11.   result = pow(2, i);
  12.   getchar();
  13.   return &result;
  14. }
  15. void getPower_r(int i, int* result)
  16. {
  17.   *result = pow(2, i);
  18. }
  19. void handler (int signal_number)        /*处理SIGALRM信号*/
  20. {
  21.   getPower(3);
  22. }
  23. int main ()
  24. {
  25.   int *result;
  26.   struct sigaction sa;
  27.   memset(&sa, 0, sizeof(sa));
  28.   sa.sa_handler = &handler;
  29.   sigaction(SIGALRM, &sa, NULL);
  30.   result = getPower(5);
  31.   printf("2^5 = %d/n", *result);
  32.   return 0;
  33. }
复制代码


试验方法:
1. 编译 gcc test.c -lpthread
在一个终端中运行 ./a.out, 在另一个终端中运行 ps -A|grep a.out可以看到该进程的id
2. 用如下方式运行a.out:
运行./a.out,在按回车前,在另外一个终端中运行kill -14 pid (这里的pid是运行上面的ps时看到的值)
然后,按回车继续运行a.out就会看到2^5 = 8 的错误结论


对于函数int* getPower(int i)

由于函数getPower会返回一个指向静态数据的指针,在第一次调用getPower的过程中,再次调用getPower,则两次返回的指针都指向同一块内存,第二次的结果将第一次的覆盖了(很多non-reentrant函数的这种用法会导致不确定的后果).所以是non-reentrant的.


对于函数void getPower_r(int i, int* result)

getPower_r会将所得的信息存储到result所指的内存中,它只是使用了由调用者提供的数据,所以是reentrant.在信号处理函数中可以正常的使用它.


2. thread-safe函数

Thread safety是多线程编程中的概念,thread safe函数是指那些能够被多个线程同时并发地正确执行的函数.

thread safe和non thread safe的例子

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <pthread.h>
  4. pthread_mutex_t sharedMutex=PTHREAD_MUTEX_INITIALIZER;
  5. int count;        /*共享数据*/
  6. void* func (void* unused)
  7. {
  8.   if (count == 0)
  9.     count++;
  10. }
  11. void* func_s (void* unused)
  12. {
  13.   pthread_mutex_lock(&sharedMutex);    /*进入临界区*/
  14.   if (count == 0)
  15.     count++;
  16.   pthread_mutex_unlock(&sharedMutex);  /*离开临界区*/
  17. }
  18. int main ()
  19. {
  20.   pthread_t pid1, pid2;
  21.   pthread_create(&pid1, NULL, &func, NULL);
  22.   pthread_create(&pid2, NULL, &func, NULL);
  23.   pthread_join(pid1, NULL);
  24.   pthread_join(pid2, NULL);
  25.   return 0;
  26. }
复制代码


函数func是non thread safe的,这是因为它不能避免对共享数据count的race condition,
设想这种情况:一开始count是0,当线程1进入func函数,判断过count == 0后,线程2进入func函数
线程2判断count==0,并执行count++,然后线程1开始执行,此时count != 0 了,但是线程1仍然要执行
count++,这就产生了错误.

func_s通过mutex锁将对共享数据的访问锁定,从而避免了上述情况的发生.func_s是thread safe的

只要通过适当的"锁"机制,thread safe函数还是比较好实现的.

3. reentrant函数与thread safe函数的区别

reentrant函数与是不是多线程无关,如果是reentrant函数,那么要求即使是同一个进程(或线程)同时多次进入该函数时,该函数仍能够正确的运作.
该要求还蕴含着,如果是在多线程环境中,不同的两个线程同时进入该函数时,该函数也能够正确的运作.

thread safe函数是与多线程有关的,它只是要求不同的两个线程同时对该函数的调用在逻辑上是正确的.

从上面的说明可以看出,reentrant的要求比thread safe的要求更加严格.reentrant的函数必是thread safe的,而thread safe的函数
未必是reentrant的. 举例说明:

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <pthread.h>
  4. #include <signal.h>
  5. #include <string.h>
  6. #include <sys/types.h>
  7. #include <unistd.h>
  8. pthread_mutex_t sharedMutex=PTHREAD_MUTEX_INITIALIZER;
  9. int count;        /*共享数据*/
  10. void* func_s (void* unused)
  11. {
  12.   pthread_mutex_lock(&sharedMutex);    /*进入临界区*/
  13.   printf("locked by thead %d/n", pthread_self());
  14.   if (count == 0)
  15.     count++;
  16.   getchar();
  17.   pthread_mutex_unlock(&sharedMutex);  /*离开临界区*/
  18.   printf("lock released by thead %d/n", pthread_self());
  19. }
  20. void handler (int signal_number)        /*处理SIGALRM信号*/
  21. {
  22.   printf("handler running in %d/n", pthread_self());
  23.   func_s(NULL);
  24. }
  25. int main ()
  26. {
  27.   pthread_t pid1, pid2;
  28.   struct sigaction sa;
  29.   memset(&sa, 0, sizeof(sa));
  30.   sa.sa_handler = &handler;
  31.   sigaction(SIGALRM, &sa, NULL);
  32.   printf("main thread's pid is: %d/n", pthread_self());
  33.   func_s(NULL);
  34.   pthread_create(&pid1, NULL, &func_s, NULL);
  35.   pthread_create(&pid2, NULL, &func_s, NULL);
  36.   pthread_join(pid1, NULL);
  37.   pthread_join(pid2, NULL);
  38.   func_s(NULL);
  39.   return 0;
  40. }
复制代码


试验方法:
1. 编译 gcc test.c -lpthread
在一个终端中运行 ./a.out, 在另一个终端中运行 ps -A|grep a.out可以看到该进程的id
2. 进行下面4次运行a.out:
每次运行分别在第1,2,3,4次回车前,在另外一个终端中运行kill -14 pid (这里的pid是上面ps中看到的值)

试验结果:
1. 该进程中有3个线程:一个主线程,两个子线程
2. func_s是thread safe的
3. func_s不是reentrant的
4. 信号处理程序会中断主线程的执行,不会中断子线程的执行
5. 在第1,4次回车前,在另外一个终端中运行kill -14 pid会形成死锁,这是因为
主线程先锁住了临界区,主线程被中断后,执行handler(以主线程执行),handler试图锁定临界区时,
由于同一个线程锁定两次,所以形成死锁
6. 在第2,3次回车前,在另外一个终端中运行kill -14 pid不会形成死锁,这是因为一个子线程先锁住
了临界区,主线程被中断后,执行handler(以主线程执行),handler试图锁定临界区时,被挂起,这时,子线程
可以被继续执行.当该子线程释放掉锁以后,handler和另外一个子线程可以竞争进入临界区,然后继续执行.
所以不会形成死锁.

结论:
1. reentrant是对函数相当严格的要求,绝大部分函数都不是reentrant的(APUE上有一个reentrant函数
的列表).
什么时候我们需要reentrant函数呢?只有一个函数需要在同一个线程中需要进入两次以上,我们才需要
reentrant函数.这些情况主要是异步信号处理,递归函数等等.(non-reentrant的递归函数也不一定会
出错,出不出错取决于你怎么定义和使用该函数). 大部分时候,我们并不需要函数是reentrant的.

2. 在多线程环境当中,只要求多个线程可以同时调用一个函数时,该函数只要是thread safe的就可以了.
我们常见的大部分函数都是thread safe的,不确定的话请查阅相关文档.

3. reentrant和thread safe的本质的区别就在于,reentrant函数要求即使在同一个线程中任意地进入两次以上,
也能正确执行.

大家常用的malloc函数是一个典型的non-reentrant但是是thread safe函数,这就说明,我们可以方便的
在多个线程中同时调用malloc,但是,如果将malloc函数放入信号处理函数中去,这是一件很危险的事情.

4. reentrant函数肯定是thread safe函数,也就是说,non thread safe肯定是non-reentrant函数
不能简单的通过加锁,来使得non-reentrant函数变成 reentrant函数
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值