可重入和线程安全

来源:互联网 发布:linux 挂起 编辑:程序博客网 时间:2024/05/17 06:50

什么是可重入?

关于可重入和不可重入这些概念网上可以找到很多,本人表达能力欠佳,所以在此处引用别人的话对这两个概念做一个简单的介绍:

在多线程或有异常控制流的情况下,当某个函数运行到中途时,控制流(也就是当前指令序列)就有可能被打断而去执行另一个函数. 如果在这种情况下不会出现问题,比如说数据或状态不会被破坏,行为确定。那么这个函数就被称做”可重入”的.函数是可重入(reentrant)的,是指对于相同的(并且合法的)函数参数(包括无参函数的情况),多次调用此函数产生的行为是可预期的,即函数的行为一致,或者结果相同。不能保证这一点的函数称为不可重入(non-reentrant)函数。

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

  • 调用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的情况,在32位系统上这种情况很容易发生,64位机器上可能出现的概率较小。这是因为在结构体赋值的过程中可能随时被信号打断,导致才赋值了部分数据,所以输出就会出现01 或 10这类情况.
下面再看第二个例子:

#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)呢?
如果一个函数在同一时刻可以被多个线程安全地调用,就称该函数是线程安全的。往往可重入和线程安全被很多人混为一谈,其实二者是两个不同的概念,可以相互组合使用。可重入函数一定是线程安全的;线程安全的函数可能是重入的,也可能是不重入的;线程不安全的函数一定是不可重入的。

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

最佳实践

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

  • 返还指定静态数据结构的指针可能会导致函数不可重入
    下面是一个不可重入版本的大小写转换
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;}
0 0
原创粉丝点击