reentrant函数与thread safe函数浅析

 
reentrant函数与thread safe函数浅析

http://www.chinaunix.net 作者:ypxing 发表于:2008-02-23 19:46:50
记得以前讨论过一个关于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,影响效率。
<think>嗯,用户问为什么Ubuntu缺少_REENTRANT和_THREAD_SAFE的定义。首先,我需要回顾之前的历史,之前提到过GNU libc已经不再需要这些宏,转而使用_POSIX_C_SOURCE。现在用户可能遇到了在Ubuntu系统中编译旧代码时,没有定义这些宏导致的问题,或者他们注意到这些宏在Ubuntu中不存在,想知道原因。 首先,我要确认Ubuntu使用的C库是glibc,而根据之前的解释,glibc已经将这些旧宏视为兼容性宏,映射到_POSIX_C_SOURCE=199506L。因此,可能用户发现他们的代码中即使没有定义这些宏,也能正常工作,或者可能在代码中显式定义它们时,Ubuntu并没有报错,但用户好奇为何系统本身不包含这些定义。 需要解释的是,这些宏原本是应用程序自己定义的,而不是由系统头文件提供的。在旧版本中,应用程序需要手动定义它们来启用线程安全,但现在glibc不再需要,所以系统本身不会定义这些宏。相反,当用户定义这些宏时,glibc会将其转换为对应的POSIX版本。 可能用户遇到的情况是,在Ubuntu上编译旧代码时,没有添加_REENTRANT定义,但代码依然正常工作,这是因为glibc已经默认处理了线程安全,或者用户代码中已经包含了其他POSIX标准的定义。需要说明的是,现代Ubuntu系统默认可能启用了更高版本的POSIX标准,所以即使没有这些宏,线程安全支持依然存在。 另外,可能用户看到某些文档或旧教程中提到需要定义这些宏,但在Ubuntu中发现不需要,因此产生疑问。需要强调这些宏的历史性和现代系统的兼容性处理,以及推荐使用_POSIX_C_SOURCE来明确标准版本。 需要确保回答结构清晰,分点说明原因,可能包括历史演变、glibc的处理方式、默认POSIX标准的提升,以及给用户的建议。同时,可能需要提醒用户如何处理遗留代码,或者在需要时如何正确设置这些宏或POSIX版本。</think>--- ### **核心原因** Ubuntu(基于 GNU libc)**不再主动定义 `_REENTRANT` 和 `_THREAD_SAFE`**,因为: 1. **历史宏的废弃**:这些宏是早期多线程编程的临时方案,现已被 POSIX 标准取代。 2. **隐式兼容性**:即使代码中手动定义它们,GNU libc 会将其**自动映射到 `_POSIX_C_SOURCE=199506L`**,无需系统预定义。 3. **现代默认行为**:Ubuntu 默认编译环境已支持多线程安全,无需依赖旧宏。 --- ### **详细解释** #### **1. 旧宏的废弃逻辑** - **原始目的**:在 90 年代的 C 库(如旧版 glibc 或 Solaris libc)中,`_REENTRANT` 和 `_THREAD_SAFE` 的作用是: - 启用线程安全函数变体(如将 `errno` 改为线程局部变量)。 - 确保头文件暴露多线程相关的 API(如 `pthread_mutex_t`)。 - **POSIX 标准化**:1995 年 POSIX.1c(对应 `_POSIX_C_SOURCE=199506L`)将多线程支持纳入标准,旧宏逐渐被淘汰。 #### **2. Ubuntu/GNU libc 的隐式处理** - **自动映射**:当代码中定义 `_REENTRANT` 或 `_THREAD_SAFE` 时,GNU libc 会: ```c #define _REENTRANT → #define _POSIX_C_SOURCE 199506L ``` 即使未显式定义这些旧宏,只要代码符合 POSIX 标准,线程安全仍会生效。 - **无系统级定义**:Ubuntu 的系统头文件(如 `<features.h>`)**不会默认定义这些宏**,因为它们已过时。 #### **3. 默认的 POSIX 版本提升** 现代 Ubuntu 版本在编译时可能隐式启用更高版本的 POSIX 标准(如 `_POSIX_C_SOURCE=200809L`),这已包含对多线程的完整支持。因此: - **无需旧宏**:即使代码未定义 `_REENTRANT`,线程安全功能仍有效。 - **兼容性保障**:若旧代码中定义了这些宏,GNU libc 会按规则提升 POSIX 版本,但不会降低标准。 --- ### **实际场景验证** #### **示例代码** ```c // test.c #include <stdio.h> #include <errno.h> int main() { printf("默认 _POSIX_C_SOURCE 值: %ld\n", _POSIX_VERSION); printf("errno 地址: %p\n", &errno); // 观察是否为线程局部存储 return 0; } ``` #### **编译运行** ```bash # 不定义任何宏 gcc test.c -pthread && ./a.out # 输出可能为:_POSIX_VERSION=200809L,errno 地址随线程变化(说明线程安全已启用) # 显式定义旧宏 gcc -D_REENTRANT test.c -pthread && ./a.out # 输出 _POSIX_VERSION=199506L,但线程安全仍有效 ``` #### **关键结论** - 即使不定义 `_REENTRANT`,Ubuntu 默认的 POSIX 版本已高于 199506L,因此线程安全机制自动生效。 - 显式定义旧宏会强制使用较低的 POSIX 版本(199506L),但不会破坏功能。 --- ### **对开发者的建议** 1. **新项目**:直接指定所需的 POSIX 版本,例如: ```c #define _POSIX_C_SOURCE 200809L // 启用 POSIX 2008 功能 #include <pthread.h> ``` 2. **旧代码迁移**: - 删除所有 `_REENTRANT` 或 `_THREAD_SAFE` 定义。 - 通过编译选项控制 POSIX 版本: ```bash gcc -D_POSIX_C_SOURCE=200809L -pthread mycode.c ``` 3. **诊断兼容性**: - 检查 `<features.h>` 中 `__USE_REENTRANT` 的隐式定义(仅供内部使用)。 - 使用 `gcc -E -dM -xc /dev/null` 查看编译器默认宏。 --- ### **总结** Ubuntu 不定义 `_REENTRANT` 和 `_THREAD_SAFE`,因为它们已被 POSIX 标准取代。现代代码应直接通过 `_POSIX_C_SOURCE` 指定标准版本,而依赖旧宏的代码仍可正常工作(GNU libc 会隐式转换)。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值