记得以前讨论过一个关于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函数放入信号处理函数中去,这是一件很危险的事情.
[ 本帖最后由 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函数要求在同一个线程中需要进入两次以上,
并能正确执行.
--------------------------------------------------------------------------------
思一克 回复于: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,影响效率。