可重入、线程安全和异步信号安全

原创 2016年03月05日 15:30:04

什么是可重入?

​   关于可重入和不可重入这些概念网上可以找到很多,这里引用一下WiKi中的解释

a computer program or subroutine is called reentrant if it can be interrupted in the middle of its execution, and then be safely called again (“re-entered”) before its previous invocations complete execution. The interruption could be caused by an internal action such as a jump or call, or by an external action such as an interrupt or signal. Once the reentered invocation completes, the previous invocations will resume correct execution.

​   简单来说是这样的,可重入指的是一个程序或者函数所具备的性质,该性质指的是一个函数在执行的过程中被中断,程序的执行流跑到另外的一个地方把这个函数重新执行了一次,执行完成后返回被中断的地方再次运行之前的函数,而这个整个过程不会影响这个被打断的函数的最终结果,那么就称这个函数是可重入的。举一个可重入函数的例子,比如计算两个值的大小,用户传入两个值,通过加法进行计算,最后返回结果,即使这个函数被中断并被再次运行,也不会影响这个函数的最终结果,因为这个函数是无状态的,如果一个函数在执行的过程中需要把计算的中间结果保存起来,那么这就不是可重入的了,比方说,gethostbyname这个函数在解析域名对应的ip地址的时会将计算的结果放在一个静态的存储中返回,如果某一个时刻调用gethostbyname,将域名已经解析好对应的ip地址了,然后放在静态存储中,在准备返回的时候被中断了,然后再次执行这个函数,但是解析的是另外一个域名,解析后的结果依然会放到静态存储中,那么这次调用就会把之前解析出来的结果给覆盖掉,本质原因就是因为这个函数是有状态的,无状态的函数一定是可重入的。

​   相信通过上面的简单介绍你或许对可重入和不可重入有了一丁丁了解,常见的不可重入的函数一般都具备以下特征:

  • 调用malloc或free (malloc内部维护了全局的链表用来管理分配的内存,这就是状态信息,free也一样)
  • 使用了静态数据结构(全局变量或静态变量)
  • 标准I/O程序库的一部分(内部会有全局锁,锁也是一种状态)
  • 调用了一个不可重入的函数

下面举一个不可重入函数的两个小例子:

#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;
struct data
{
        int a;
        int b;
}da;

void handler(int signum)
{
        cout << "data:" << da.a << da.b << endl;
        alarm(1);
}

int main()
{
        static data zeros;
        zeros.a = 0;
        zeros.b = 0;
        static data ones;
        ones.a = 1;
        ones.b = 1;
        signal(SIGALRM,handler);
        da = zeros;
        alarm(1);
        while(1)
        {
                da = zeros;
                da = ones;
        }
}

​   程序的预期的结果应该是00 11交替输出,然后在这里结果却不一定是这样的,这是因为在结构体赋值的过程中可能随时被信号打断,导致才赋值了部分数据,所以输出就会出现01 或 10这类情况.(这取决于硬件和OS,不同的硬件和OS对于对其的8位、16位、32位数据的读写不一定是原子的),比如说在32位平台上操作64位的整形,这就不是原子的,赋值和读取操作都至少需要两条指令,这就会存在只赋值了部分数据的情况。

下面再看第二个例子:

#include <iostream>
#include <netdb.h>
#include <signal.h>
#include <unistd.h>

using namespace std;

void handler(int signum)
{
        hostent *hostptr;
        hostptr = gethostbyname("www.51cto.com");
        cout << hostptr->h_name << endl;
        alarm(1);
}

int main()
{
        hostent *hostptr;
        signal(SIGALRM,handler);
        alarm(1);
        while(1)
        {
                hostptr = gethostbyname("www.baidu.com");
                sleep(1);
        }
}

​   同样在这个例子中gethostbyname本身就是一个不可重入函数,这个函数的实现机制是将得到的结果保存在一个静态变量中,那么这就容易导致一个问题,假设此时gethostname解析出结果存入静态变量中,在函数没返回之前被中断,中断处理函数中再次调用gethotsbyname改变了静态变量的值,信号返回到被中断处,中断处的gethostbyname返回的则是信号处理函数中gethostbyname设置的结果

可重入和线程安全

​   什么是线程安全的(Thread-Safe)呢?如果一个函数在同一时刻可以被多个线程安全地调用,就称该函数是线程安全的。往往可重入和线程安全被很多人混为一谈,其实二者是两个不同的概念,可以相互组合使用。WiKi中是这样解释两者的关系的:

Reentrancy is distinct from, but closely related to, thread-safety. A function can be thread-safe and still not reentrant. For example, a function could be wrapped all around with a mutex (which avoids problems in multithreading environments), but if that function is used in an interrupt service routine, it could starve waiting for the first execution to release the mutex. The key for avoiding confusion is that reentrant refers to only one thread executing. It is a concept from the time when no multitasking operating systems existed.

​   可重入和线程安全是两个不同的概念,但是很相近,一个函数可以是线程安全但不是可重入的,例如一个函数通过mutex来保护某些互斥资源,从线程安全的角度来看这个函数是线程的,但是因为使用了mutex,使得函数是有状态的了,很自然这个函数不是线程安全的。可重入适用于单线程和多线程,而线程安全特指多线程环境下一个函数是否可以被安全的调用。

可重入和异步信号安全

​   异步信号安全指的是可以在信号处理器中可以被安全调用的函数,可重入函数满足了这个特点,通常来说不可重入的函数不是异步信号安全的,为了满足异步信号安全的要求一般会使用以下两个方法:

  1. 要求信号处理函数本身必须是可重入的,然后,在信号处理函数中不要去调用不可重入的函数。
  2. 函数在在处理一些全局数据时,要进行信号屏蔽,等处理完后再开启信号

​   通过上面对异步信号安全的解释,我认为可重入函数一定是异步信号安全函数,但是异步信号安全函数不一定是可重入的,因为程序中断不仅仅只有信号这一种,异步信号安全的函数是可以通过信号屏蔽的方法使得函数本身变成异步信号安全的。而可重入函数需要处理在任何类型的中断情况下都是可重入的。

最佳实践

​   如何写出一个可重入的函数,是一个需要仔细探讨的问题,要写一个可重入函数就不要在函数内部使用静态或全局数据,不要返回静态或全局数据,也不调用不可重入函数。而要实现线程安全的函数就需要解决多个线程调用函数时访问资源冲突的问题(加锁,线程局部存储或许是大家经常使用的手段).

通过总结出实际工程项目开发中的一些工程实践经验,可以给我们编写可重入函数提供一些指导性意见.

  • 返还指定静态数据结构的指针可能会导致函数不可重入
    下面是一个不可重入版本的大小写转换
char * strtoupper(char *str)
{
        static char buf[BUFSIZ];
        int index;
        for(index = 0; str[index];index++)
        {
                buf[index] = toupper(str[index]);
        }
        buf[index] = 0;
        return buf;
}

修改为可重入版本

char * strtoupper_r(char *str,char *dst)
{
        int index;
        for(index = 0; str[index];index++)
        {
                dst[index] = toupper(str[index]);
        }
        dst[index] = 0;
        return dst
}
  • 在函数中记忆(保存)数据的状态导致不可重入
char GetLowerCaseChar(char *str)
{
        static index = 0;
        char c = 0;
        while(c = str[index])
        {
                if(islower(c))
                {
                        index++;
                        break;
                }
                index++
        }
        return c;
}

​   这是一个搜索字符串中小写字符的程序,函数保存了搜索到字符串的位置index,导致这个函数是不可重入的这个index应该由调用者来维护,下面是这个函数的可重入版本:

char GetLowerCaseChar(char *str,int *index)
{
        char c = 0;
        while(c = str[*index])
        {
                if(islower(c))
                {
                        (*index)++;
                        break;
                }
                (*index)++
        }
        return c;
}
  • 任何分配和释放内存的库函数都是不可重入的
  • 小心处理进程范围内的全局变量入(例如 errno 和h_errno)
if(open(filename,O_CREAT|O_RDWR) < 0 ){
    perror("open file fail:");
    exit(-1);
}

​   上面这个函数就是一个不可重入的函数,当在执行open完,因为某些原因导致open失败结果就是改变了errno错误码,但此时被信号中断,去执行信号处理函数,信号处理函数中执行了一些系统调用或者库函数导致了errno全局状态码发生了变化,那么等待信号处理函数执行完毕返还后那么open失败的错误码就被覆盖了。为了避免上述问题发生应该在信号处理函数中保存errno状态码,执行完毕后进行恢复。

void handler(int signo)
{
    int errno_saved;
    errno_saved = errno;
    /*
        执行一些业务
    */
    //状态码恢复
    errno = errno_saved;
}
版权声明:本文为博主原创文章,未经博主允许不得转载。

相关文章推荐

C++11系列-区间迭代

C++11系列-区间迭代 2013-08-20 在我前面介绍C++11的文章中,我提到C++11将会带来一些实用的改进。我的意思是它将移除一些不必要的打字和其它影响快速编码的壁垒。我前...

source insight中cpp文件和h文件的切换(使用si的内置语言实现)[增加src/include目录切换]

注:更新后的脚本,支持原路径中是include或src的目录切换,而不再要求是在同一目录中的cpp和h文件切换, 相对第一版的修改部分的代码后面也用红色标注。研究了si的编程语言,总算实现了...
  • Jaogoy
  • Jaogoy
  • 2009年12月24日 11:23
  • 4310

异步信号安全(可重入性)与线程安全

书中10.6和12.5两节分别是信号和线程的重入介绍。但是未对异步信号安全、线程安全、可重入概念做统一对比,难以彻悟。针对于此,写下本文。 1. 三个概念,线程安全,可重入,信号安全   ...
  • julysee
  • julysee
  • 2015年04月23日 15:26
  • 245

线程安全与重入以及异步信号安全的区别.

可重入一定是线程安全的,但是线程安全不一定是可重入的. http://www.cnblogs.com/baizx/p/5128862.html  线程安全:        线程安全函数:...

可重入,异步信号安全,线程安全

可重入,异步信号安全,线程安全学习小结。

linux可重入、异步信号安全和线程安全

一 可重入函数

可重入、线程安全、异步信号安全

http://blog.csdn.net/littlehedgehog/archive/2009/04/23/4104210.aspx http://docs.sun.com/app/docs/doc...
  • bytxl
  • bytxl
  • 2012年07月27日 13:52
  • 657

Linux日常——信号(3)之线程安全和可重入函数

在深入讲解今天的题目前,我们需要有以下的知识储备:捕捉信号如果信号的处理动作是⽤用户⾃自定义函数,在信号递达时就调⽤用这个函数,这称为捕捉信号。 信号捕捉我在我的第一篇信号博客这有提到过,不过当时只...

Java并发:可重入与线程安全

转载请注明出处:jiq•钦'stechnical Blog1函数的可重入性:函数可重入,意味着该函数可以同时由多个任务调用,而不会产生任何错误。可重入的函数不需要考虑线程安全问题,因为其不会引用任何共...

可重入函数和线程安全的区别与联系

1 )什么是可重入函数?        可重入函数即表示可以被多个执行流重复进入,意味着只使用自己栈上的变量,可以允许有该函数的多个副本在运行,由于它们使用的是分离的栈,所以不会互相干扰。 举一个不...
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:可重入、线程安全和异步信号安全
举报原因:
原因补充:

(最多只允许输入30个字)