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

原创 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;
}
版权声明:本文为博主原创文章,未经博主允许不得转载。 举报

相关文章推荐

每周荐书:Java Web、Python极客编程、移动Web前端(评论送书)

每周荐书:Java Web、Python极客编程、移动Web前端(评论送书) 各位抱歉,上周活动停更一周,本周继续感谢大家对每周荐书栏目的支持,先公布下上期活动中奖名单 微雨燕凝霜寒森林之舟架构探险:...

Linux定时器描述符(timerfd)shiy

最近,学习、研究Linux基于文件描述符的定时器应用。相关介绍N多,这里将碰到的问题,要注意的地方,说明下    这个主要有三个相关函数: 1、timerfd_create(int clockid, ...

我是如何成为一名python大咖的?

人生苦短,都说必须python,那么我分享下我是如何从小白成为Python资深开发者的吧。2014年我大学刚毕业..

linux新API---timerfd的使用方法

timerfd是Linux为用户程序提供的一个定时器接口。这个接口基于文件描述符,通过文件描述符的可读事件进行超时通知,所以能够被用于select/poll的应用场景。timerfd是linux内核2...

timerfd使用总结

timerfd是Linux为用户程序提供的一个定时器接口。这个接口基于文件描述符,通过文件描述符的可读事件进行超时通知,所以能够被用于select/poll的应用场景。timerfd是linux内核2...

timerfd API使用总结

timerfd 介绍timerfd 是在Linux内核2.6.25版本中添加的接口,其是Linux为用户提供的一个定时器接口。这个接口基于文件描述符,所以可以被用于select/poll/epoll的...

timerfd

timerfd是Linux为用户程序提供的一个定时器接口。这个接口基于文件描述符,所以能够被用于select/poll的应用场景。 1.      使用方法 timerfd提供了如下接口供用户...

Linux的timerfd分析

timerfd是Linux为用户程序提供的一个定时器接口。这个接口基于文件描述符,所以能够被用于select/poll的应用场景。1.      使用方法timerfd提供了如下接口供用户使用time...

timerfd使用总结

timerfd是Linux为用户程序提供的一个定时器接口。这个接口基于文件描述符,通过文件描述符的可读事件进行超时通知,所以能够被用于select/poll的应用场景。timerfd是linux内核2...

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

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

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

可重入,异步信号安全,线程安全学习小结。
返回顶部
收藏助手
不良信息举报
您举报文章:深度学习:神经网络中的前向传播和反向传播算法推导
举报原因:
原因补充:

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