reentrant函数与thread safe函数浅析

时间:2008-01-16 11:13:49  来源:  作者:

记得以前讨论过一个关于reentrant函数与thread safe函数的帖子
很多人对于这两种函数不是很了解,
尤其是发现malloc等函数是non-reentrant函数时,对多线程编程都产生了"恐惧"
这里是我对这两种函数的一些理解,希望和大家探讨一些.欢迎批评指正.

1. reentrant函数

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

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

reentrant和non-reentrant函数的例子



#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <math.h>

int* getPower(int i)
{
  static int result;
  result = pow(2, i);
  getchar();
  return &result;
}

void getPower_r(int i, int* result)
{
  *result = pow(2, i);
}

void handler (int signal_number) /*处理SIGALRM信号*/
{
  getPower(3);
}

int main ()
{
  int *result;
  struct sigaction sa;
  memset(&sa, 0, sizeof(sa));
  sa.sa_handler = &handler;
  sigaction(SIGALRM, &sa, NULL);
  result = getPower(5);
  printf("2^5 = %dn", *result);
  return 0;
}



试验方法:
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的例子



#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

pthread_mutex_t sharedMutex=PTHREAD_MUTEX_INITIALIZER;

int count; /*共享数据*/

void* func (void* unused)
{
  if (count == 0)
    count++;
}

void* func_s (void* unused)
{
  pthread_mutex_lock(&sharedMutex);    /*进入临界区*/
  if (count == 0)
    count++;
  pthread_mutex_unlock(&sharedMutex);  /*离开临界区*/
}


int main ()
{
  pthread_t pid1, pid2;
  pthread_create(&pid1, NULL, &func, NULL);
  pthread_create(&pid2, NULL, &func, NULL);
  pthread_join(pid1, NULL);
  pthread_join(pid2, NULL);
  return 0;
}



函数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的. 举例说明:



#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <signal.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>

pthread_mutex_t sharedMutex=PTHREAD_MUTEX_INITIALIZER;

int count; /*共享数据*/

void* func_s (void* unused)
{
  pthread_mutex_lock(&sharedMutex);    /*进入临界区*/
  printf("locked by thead %dn", pthread_self());
  if (count == 0)
    count++;
  getchar();
  pthread_mutex_unlock(&sharedMutex);  /*离开临界区*/
  printf("lock released by thead %dn", pthread_self());
}

void handler (int signal_number) /*处理SIGALRM信号*/
{
  printf("handler running in %dn", pthread_self());
  func_s(NULL);
}


int main ()
{
  pthread_t pid1, pid2;
  struct sigaction sa;
  memset(&sa, 0, sizeof(sa));
  sa.sa_handler = &handler;
  sigaction(SIGALRM, &sa, NULL);
  printf("main thread's pid is: %dn", pthread_self());
  func_s(NULL);
  pthread_create(&pid1, NULL, &func_s, NULL);
  pthread_create(&pid2, NULL, &func_s, NULL);
  pthread_join(pid1, NULL);
  pthread_join(pid2, NULL);
  func_s(NULL);
  return 0;
}



试验方法:
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/aixprggd/genprogc/writing_reentrant_thread_safe_code.htm

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



 lenovo 回复于:2007-08-02 21:38:57

不错,很好的帖子。


 科技牛 回复于:2007-08-03 15:38:14

受教很深!


 ypxing 回复于:2007-08-03 15:58:22

调用了malloc的函数肯定是non-reentrant的

引用:原帖由 bluster 于 2007-8-3 15:55 发表 [url=http://bbs.chinaunix.net/redirect.php?goto=findpost&pid=7155171&ptid=971102]

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




 ypxing 回复于:2007-08-03 15:59:35

这家伙,怎么把自己的帖子给删了?


 bluster 回复于:2007-08-03 16:01:11

引用:原帖由 ypxing 于 2007-8-3 15:58 发表 [url=http://bbs.chinaunix.net/redirect.php?goto=findpost&pid=7155198&ptid=971102]
调用了malloc的函数肯定是non-reentrant的

 


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


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

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


 jigloo 回复于:2007-08-03 16:11:55

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

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


 思一克 回复于:2007-08-03 17:03:49

这个问题很复杂。

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

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

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

类似的例子:
[CODE]
int ia[32];

int func(int i)
{
    ia++;
    printf("%p i %d %dn", &i, i, ia);
    if(i == 31) return;
    func(i+1);
}

main()
{
    func(0);

}
[/CODE]

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


 ypxing 回复于:2007-08-03 18:50:12

思一克,你好
首先谢谢你的鼓励.

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

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

而且也不是线程安全的.

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

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

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


int ia[32];

int func(int i)
{
    ia++;
    printf("%p i %d %dn", &i, i, ia);
    if(i == 31) return;
    func(i+1);
}

main()
{
    func(0);

}



 思一克 回复于:2007-08-03 19:39:57

你写可重入函数时候要考虑到保证不可重入部分不重入, 还有保证整个函数必须可重入.
__do_IRQ就是如此.
所以说"调用了不可重入函数的函数不一定是不可重入的"是正确的.
而"调用了不可重入函数的函数一定是不可重入的"是不对的.因为有十分多的反例.


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


 feasword 回复于:2007-08-03 20:09:35

一直想找这两个概念是此非彼的例子,受教了
关于死锁的问题,apue里也有讲,以前也遇到过,当时干脆都弄成递归锁了


 ypxing 回复于:2007-08-03 20:49:04

那么,怎么才能保证不可重入的部分不被重入呢?

引用:原帖由 思一克 于 2007-8-3 19:39 发表 [url=http://bbs.chinaunix.net/redirect.php?goto=findpost&pid=7156157&ptid=971102]
你写可重入函数时候要考虑到保证不可重入部分不重入, 还有保证整个函数必须可重入.
__do_IRQ就是如此.
所以说"调用了不可重入函数的函数不一定是不可重入的"是正确的.
而"调用了不可重入函数的函数一定是不可 ... 




 cugb_cat 回复于:2007-08-03 22:12:05

引用:原帖由 ypxing 于 2007-8-3 20:49 发表 [url=http://bbs.chinaunix.net/redirect.php?goto=findpost&pid=7156295&ptid=971102]
那么,怎么才能保证不可重入的部分不被重入呢?

 


我有同楼主相同的疑问。

另外,从lz的例子中学到一些技巧,关于调试多线程程序,感谢lz。

[ 本帖最后由 cugb_cat 于 2007-8-3 22:45 编辑 ]


 飞灰橙 回复于:2007-08-03 22:18:09

引用:原帖由 思一克 于 2007-8-3 19:39 发表 [url=http://bbs.chinaunix.net/redirect.php?goto=findpost&pid=7156157&ptid=971102]
你写可重入函数时候要考虑到保证不可重入部分不重入, 还有保证整个函数必须可重入.
__do_IRQ就是如此.
所以说"调用了不可重入函数的函数不一定是不可重入的"是正确的.
而"调用了不可重入函数的函数一定是不可重入的"是不对的(语句A).因为有十分多的反例.


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



越看越糊涂了,撇开讨论的问题不谈, 上面的语句A和语句B,必定有一句是错的


 cugb_cat 回复于:2007-08-03 22:44:57

引用:原帖由 飞灰橙 于 2007-8-3 22:18 发表 [url=http://bbs.chinaunix.net/redirect.php?goto=findpost&pid=7156605&ptid=971102]


越看越糊涂了,撇开讨论的问题不谈, 上面的语句A和语句B,必定有一句是错的 


两句意思相反~:mrgreen:


 ypxing 回复于:2007-08-03 23:30:14

俺也看了好一会才看懂:em02: 

引用:原帖由 飞灰橙 于 2007-8-3 22:18 发表 [url=http://bbs.chinaunix.net/redirect.php?goto=findpost&pid=7156605&ptid=971102]


越看越糊涂了,撇开讨论的问题不谈, 上面的语句A和语句B,必定有一句是错的 




 mingyanguo 回复于:2007-08-04 00:08:35

完了,简单的问题复杂化了 :mrgreen:


 hakase 回复于:2007-08-08 20:37:06

好帖,受教了~~


 ypxing 回复于:2007-08-08 23:05:51

这两天写了一个测试程序来验证malloc的不可重入性
但是malloc一直没有crash,有点郁闷

过段时间把自己的测试代码贴出来,让大家来帮忙看看


 bluster 回复于:2007-08-09 10:08:56

引用:原帖由 ypxing 于 2007-8-8 23:05 发表 [url=http://bbs.chinaunix.net/redirect.php?goto=findpost&pid=7176529&ptid=971102]
这两天写了一个测试程序来验证malloc的不可重入性
但是malloc一直没有crash,有点郁闷

过段时间把自己的测试代码贴出来,让大家来帮忙看看 


多线程条件下,signal的handler有可能在一个单独的线程中执行,如果这样那么malloc用锁保护就够了。


 ypxing 回复于:2007-08-09 10:29:51

在多线程条件下,
理论上,将malloc放入signal的handler也是会出问题的,
锁是不行的,会死锁

引用:原帖由 bluster 于 2007-8-9 10:08 发表 [url=http://bbs.chinaunix.net/redirect.php?goto=findpost&pid=7177603&ptid=971102]

多线程条件下,signal的handler有可能在一个单独的线程中执行,如果这样那么malloc用锁保护就够了。 




 ypxing 回复于:2007-08-09 16:22:20

试图测试malloc不可重入性的代码如下:


main.c
/*这是主程序,用来调用malloc*/

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>

void setUnblock()
{
  sigset_t sigset;
  sigemptyset(&sigset);
  sigprocmask(SIG_SETMASK, &sigset, NULL);
      
}


void usr1Handler (int signal_number)        /*处理SIGUSR1信号*/
{
  setUnblock(); /*使得SIGUSR1可以被嵌套*/
  free((int*)malloc(sizeof(int)*1000));
  //printf("enter handlern");
  //getchar();
}

int main ()
{
  int *pi;
  struct sigaction sa;
  
  memset(&sa, 0, sizeof(sa));
  sa.sa_handler = &usr1Handler;
  sigaction(SIGUSR1, &sa, NULL);

  pause();
  
  return 0;
}





kill.c 

/*这个是用来发送SIGUSR1信号的*/
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

int main(int argc,char *argv[])
{
  int i;
  char killstr[30]="kill -USR1 ";
  if (argc == 2)
  {
    strcat(killstr, argv[1]);
  }
   for (i=0; i<3; i++)
  {
   fork();        /*这样会有8个进程同时发送*/
  }
  
  while(1)
  {
    system(killstr);
  }
  
  return 0;
}



验证方法是:
1. 编译main.c 和kill.c
gcc main.c -o main
gcc kill.c -o kill

2. 运行./main
并在另外一个终端运行ps -A|grep main查找出该进程的进程号为pid

3. 运行./kill pid (此处pid为第二步查到的pid)

运行了很长时间,也没有crash
请大家看看我的程序,讨论一个测试方案出来

引用:原帖由 ypxing 于 2007-8-8 23:05 发表 [url=http://bbs.chinaunix.net/redirect.php?goto=findpost&pid=7176529&ptid=971102]
这两天写了一个测试程序来验证malloc的不可重入性
但是malloc一直没有crash,有点郁闷

过段时间把自己的测试代码贴出来,让大家来帮忙看看 




 mingyanguo 回复于:2007-08-09 17:36:23

引用:原帖由 ypxing 于 2007-8-9 16:22 发表 [url=http://bbs.chinaunix.net/redirect.php?goto=findpost&pid=7180404&ptid=971102]
试图测试malloc不可重入性的代码如下:


main.c
/*这是主程序,用来调用malloc*/

#include 
#include 
#include 
#include 
#include 
#include 

void setUnblock()
{
  sigset_t sigset;
  s ... 


我估计是因为现在的malloc是线程安全的原因所以不会crash但是死锁。
我在debian上面的一个测试代码,会死锁,top一下会发现进程状态总是sleep

#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>

#if 0
#define PRINT(a) do {
printf a;
fflush(stdout);
}while(0)
#else
#define PRINT(a)
#endif

static void
run_malloc(void)
{
void *mem[8];
int sz;
int i;

for (i = 0; i < (sizeof(mem)/sizeof(mem[0])); i++) {
sz = random() % (1024 * 1024);
if (sz <= 0)
sz = 1024;
mem = malloc(sz);
if (mem == NULL) {
PRINT (("[%d] malloc null...n", i));
exit(-1);
}
PRINT(("%dn", i));
snprintf(mem, sz, "this is a test...");
}

for (--i; i >= 0; i--) {
free(mem);
}
}

static void
sighandler(int signo)
{
static void *mem = NULL;

PRINT ((".n"));
if (mem == NULL) {
mem = malloc(1024);
} else {
free(mem);
mem = NULL;
}
}

static void
malloc_loop(void)
{

for (;;)
run_malloc();
}

static void
signal_loop(pid_t child)
{
int usec;

for (;;) {
kill(child, SIGUSR1);
usec = ((unsigned int)random()) % 10;
usleep(usec);
}
}

int
main(int argc, char **argv)
{
pid_t child;

if ((child = fork()) < 0) {
perror("fork()");
exit(-1);
} else if (child == 0) {
/* child */
if (signal(SIGUSR1, sighandler) < 0) {
perror("signal");
exit(-1);
}
malloc_loop();
} else {
/* parent */
signal_loop(child);
}

return 0;
}





 haohao06 回复于:2007-08-10 11:45:06

谢谢楼主讲解.收藏先




原文链接:http://bbs.chinaunix.net/viewthread.php?tid=971102
转载请注明作者名及原文出处


文章评论

共有 位网友发表了评论 查看完整内容