免费注册 查看新帖 |

Chinaunix

  平台 论坛 博客 文库
123下一页
最近访问板块 发新帖
查看: 21238 | 回复: 27
打印 上一主题 下一主题

[函数] [原创]reentrant函数与thread safe函数浅析 [复制链接]

论坛徽章:
0
跳转到指定楼层
1 [收藏(0)] [报告]
发表于 2007-08-02 21:28 |只看该作者 |倒序浏览
记得以前讨论过一个关于reentrant函数与thread safe函数的帖子
很多人对于这两种函数不是很了解,
尤其是发现malloc等函数是non-reentrant函数时,对多线程编程都产生了"恐惧"
这里是我对这两种函数的一些理解,希望和大家探讨一些.欢迎批评指正.

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函数
这个链接是说明一些non-reentrant ===> reentrant和non thread safe ===>thread safe转换的
http://www.unet.univie.ac.at/aix ... hread_safe_code.htm

[ 本帖最后由 ypxing 于 2007-8-4 01:06 编辑 ]

论坛徽章:
1
荣誉版主
日期:2011-11-23 16:44:17
2 [报告]
发表于 2007-08-02 21:38 |只看该作者
不错,很好的帖子。

论坛徽章:
0
3 [报告]
发表于 2007-08-03 15:38 |只看该作者
受教很深!

论坛徽章:
0
4 [报告]
发表于 2007-08-03 15:58 |只看该作者
调用了malloc的函数肯定是non-reentrant的

原帖由 bluster 于 2007-8-3 15:55 发表

最后一点是错的,比如一个函数调用malloc并不影响这个函数是否是reentrant。

论坛徽章:
0
5 [报告]
发表于 2007-08-03 15:59 |只看该作者
这家伙,怎么把自己的帖子给删了?

论坛徽章:
0
6 [报告]
发表于 2007-08-03 16:01 |只看该作者
原帖由 ypxing 于 2007-8-3 15:58 发表
调用了malloc的函数肯定是non-reentrant的


你是对的,我一时有点绕。
其实,是对reentrant的定义有问题。
可重入的意思,差不多是函数的任意部分都可以并行,而线程安全的意思则是多线程环境下使用没有问题,对于非可重入的函数,使用lock来保护不可并行的部分从而线程安全。
原帖由 ypxing 于 2007-8-3 15:59 发表
这家伙,怎么把自己的帖子给删了?

无价值糊涂帖,所以删了。

[ 本帖最后由 bluster 于 2007-8-3 16:05 编辑 ]

论坛徽章:
0
7 [报告]
发表于 2007-08-03 16:11 |只看该作者
>>3. reentrant和thread safe的本质的区别就在于,reentrant函数要求在同一个线程中需要进入两次以上,
并能正确执行.

这个说的不对,可重入区别在于允许任意中断函数的执行并恢复(比如信号)
http://www.ibm.com/developerworks/cn/linux/l-reent.html

论坛徽章:
0
8 [报告]
发表于 2007-08-03 17:03 |只看该作者
这个问题很复杂。

LZ的帖子很好。改进的地方是LZ应该多讲WHY不可重入,如何才可重入,而不是下结论。

1)调用了不可重入函数的函数不一定是不可重入的。比如LINUX KERNEL中,设备中断处理函数是不可重入的,而__do_IRQ()调用了他们,但__do_IRQ却是可重入的。
只要保证被调用的函数部分没有重入就可以了。

2)使用的全局变量的函数也不一定是不可重入的。还比如__do_IRQ()使用了全局变量来存储数据,但它是可重入的。

类似的例子:

  1. int ia[32];

  2. int func(int i)
  3. {
  4.     ia[i]++;
  5.     printf("%p i %d %d\n", &i, i, ia[i]);
  6.     if(i == 31) return;
  7.     func(i+1);
  8. }

  9. main()
  10. {
  11.     func(0);

  12. }
复制代码


关于这个问题,看LINUX中断处理部分非常有启发。那里逻辑复杂,各种重入(硬,软中断,多CPU)处理的非常巧妙。

论坛徽章:
0
9 [报告]
发表于 2007-08-03 18:50 |只看该作者
思一克,你好
首先谢谢你的鼓励.

你给出的这个例子,函数func,既不是可重入的,也不是线程安全的,
原因如下:

假设有一个信号处理函数handler,里面调用了func
考虑这种情况:
主函数中调用了func(0) (这个时候,你的本意是先要ia[0]++,然后打印现在ia[0]的值,
再然后继续后面的操作),
在func刚执行完ia[0]++时,信号触发了handler函数,
handler函数会调用func函数,然后执行对ia的一系列操作,完成后返回.
这时,你的主函数调用的func继续执行,也就是要printf了,
这时printf的东东就不是你想要的了,而且你无法确定现在ia[0]的值是什么(因为信号
可以中断很多次很多层).所以func不是可重入的.

而且也不是线程安全的.

可重入的一个判定方法就是将它放入信号处理函数中,仔细推敲各种中断情况下,
你是不是还能得到你想要的结果.

"使用的全局变量的函数也不一定是不可重入的。"这句是正确的,只要正确使用就可以了,
但是不使用全局变量是写可重入函数的简单方法.

"调用了不可重入函数的函数不一定是不可重入的。"这句是不对的,
因为你无法保证被调用的不可重入函数部分不被重入


  1. int ia[32];

  2. int func(int i)
  3. {
  4.     ia[i]++;
  5.     printf("%p i %d %d\n", &i, i, ia[i]);
  6.     if(i == 31) return;
  7.     func(i+1);
  8. }

  9. main()
  10. {
  11.     func(0);

  12. }
复制代码

论坛徽章:
0
10 [报告]
发表于 2007-08-03 19:39 |只看该作者
你写可重入函数时候要考虑到保证不可重入部分不重入, 还有保证整个函数必须可重入.
__do_IRQ就是如此.
所以说"调用了不可重入函数的函数不一定是不可重入的"是正确的.
而"调用了不可重入函数的函数一定是不可重入的"是不对的.因为有十分多的反例.


调用了不可重入函数的函数不一定是不可重入的。"这句是不对的,
因为你无法保证被调用的不可重入函数部分不被重入
您需要登录后才可以回帖 登录 | 注册

本版积分规则 发表回复

  

北京盛拓优讯信息技术有限公司. 版权所有 京ICP备16024965号-6 北京市公安局海淀分局网监中心备案编号:11010802020122 niuxiaotong@pcpop.com 17352615567
未成年举报专区
中国互联网协会会员  联系我们:huangweiwei@itpub.net
感谢所有关心和支持过ChinaUnix的朋友们 转载本站内容请注明原作者名及出处

清除 Cookies - ChinaUnix - Archiver - WAP - TOP