reentrant函数与thread safe函数浅析
记得以前讨论过一个关于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 = %d\n", *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 %d\n", pthread_self()); if (count == 0) count++; getchar(); pthread_mutex_unlock(&sharedMutex); /*离开临界区*/ printf("lock released by thead %d\n", pthread_self()); }
void handler (int signal_number) /*处理SIGALRM信号*/
{ printf("handler running in %d\n", 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: %d\n", 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 %d\n", &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 %d\n", &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 handler\n"); //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(("%d\n", 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
谢谢楼主讲解.收藏先
-------------------------------------------------------------------------------- system888net 回复于:2008-02-23 12:12:14
顶...
-------------------------------------------------------------------------------- dxcnjupt 回复于:2008-02-23 19:46:50
不知道这个理解对不对:
thread-safe和reentrant的区别:在发生中断时,高优先级代码抢占,此时若低优先级代码持有锁,则高优先级代码会一直等待锁打开,但是低优先级代码失去了调度机会,于是造成死锁。thread-safe不考虑这种情况,但是reentrant需要。
实现reentrant的几种方法:
1不使用临界区,把原先的全局/静态变量变成函数参数,由函数调用者维护。优点是实现简单,缺点是函数功能的封装性可能会受到影响。 2在进入临界区之前,关中断(屏蔽信号)。优点是实现简单,缺点是影响实时性能,在多核机器上可能引起瓶颈(几个核等待一个核释放信号量)。 3尝试加锁,无法加锁返回一个出错值,而不是一直等待下去。缺点是出错处理比较麻烦 4为一组临界量开启一个专门的线程进行处理。优点是可以对临界区的访问按优先级排序,以及其它可扩展操作,缺点是性能受到IPC的影响。 5使用lock-free结构取代锁。缺点是lock-free算法很多都需要memory-copy,影响效率。 |