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

http://blog.csdn.net/littlehedgehog/archive/2009/04/23/4104210.aspx

http://docs.sun.com/app/docs/doc/816-5137/6mba5vqbd?l=en&a=view#gen-95948

http://hi.baidu.com/firobd/item/e45d7f8a68005f57840fabe2

http://blog.sina.com.cn/s/blog_8fa7dd4101015hi5.html

对于可重入、线程安全、异步信号安全几个概念的理解
可重入与异步信号安全
一个可重入的函数简单来说就是可以被中断的函数,也就是说,可以在这个函数执行的任何时刻中断它,转入OS调度下去执行另外一段代码,而返回控制时不会出现什么错误。
《多线程编程指南》中定义,可以被信号控制器安全调用的函数被称为"异步信号安全"函数。
因此,我认为可重入与异步信号安全是一个概念
有人将可重入函数与线程安全函数混为一谈,我认为是不正确的。
这里引用CSAPP(深入理解计算机系统)中的描述来说明一下:
--------------------------------------------------
CSAPP
13.7.1 线程安全
一个函数被称为线程安全的,当且仅当被多个并发线程反复的调用时,它会一直产生正确的结果。
13.7.2 可重入性
有一类重要的线程安全函数,叫做可重入函数,其特点在于它们具有一种属性:当它们被多个线程调用时, 不会引用任何共享的数据
尽管线程安全和可重入有时会(不正确的)被用做同义词,但是它们之间还是有清晰的技术差别的。 可重入函数是线程安全函数的一个真子集

--------------------------------------------------

Writing Reentrant and Thread-Safe Code

在单线程程序中,只有单一控制流,程序所执行的代码不必是可重入或线程安全的。在多线程程序中,同一函数和同一资源有可能被多个控制流并发访问。为了保证资源的完整性,多线程程序中所使用的代码必须是可重入和线程安全的。

 

本节提供了编写可重入和线程安全程序的相关信息。然而本节的主题并不是如何编写高效并行化的多线程程序,这只有在程序设计阶段才能完成。现有的单线程程序必须彻底的重新设计和重新编写,才能实现高效线程化。

 

理解可重入与线程安全

 

可重入与线程安全这两个概念,都与函数处理资源的方式有关。可重入与线程安全是两个独立的概念,一个函数可以是可重入或是线程安全,或是同时满足两者,或是同时不满足两者的。

 

可重入

一个可重入的函数在执行中并不使用静态数据,也不返回指向静态数据的指针。所有使用到的数据都由函数的调用者提供。可重入函数在函数体内不能调用非可重入函数。

一个非可重入函数通常(尽管不是所有情况下)由它的外部接口和使用方法即可进行判断。例如 strtok()是非可重入的,因为它在内部存储了被标记分割的字符串;ctime()函数也是非可重入的,它返回一个指向静态数据的指针,而该静态数据在每次调用中都被覆盖重写。

 

线程安全

一个线程安全的函数通过加锁的方式来实现多线程对共享数据的安全访问。线程安全这个概念,只与函数的内部实现有关,而不影响函数的外部接口

C语言中,局部变量是在栈上分配的。因此,任何未使用静态数据或其他共享资源的函数都是线程安全的。例如,下面的函数是线程安全的:

int diff(int x, int y)

{

        intdelta;

        delta= y - x;

        if (delta < 0)

                delta= -delta;

        return delta;

}

使用全局变量(的函数)是非线程安全的。这样的信息应该以线程为单位进行存储,这样对数据的访问就可以串行化。一个线程可能会读取由另外一个线程生成的错误代码。在AIX中,每个线程有独立的errno变量。

 

函数可重入化

在多数情况下,非可重入的函数必须被修改过的具有可重入接口的函数所替代。非可重入函数不可用于多线程环境。此外,一个非可重入的函数可能无法满足线程安全的要求。

     返回数据

 

很多非可重入函数返回指向静态数据的指针。可以以两种方式避免这种情况:

   返回指向动态分配空间的指针。在这种情况下,由调用者负责释放资源。这种方式的优点在于函数的外部接口不用修改。然后,却无法保证代码的向后兼容:调用修改后函数的单线程程序,如果不做修改的话来释放资源的话,会出现内存泄露的问题。

    使用由调用提供的存储空间。尽管函数的外部接口需要改动,但是该方法是被推荐的。   

例如,将字符串大写化的strtoupper()函数,实现如下:

char *strtoupper(char *string)

{

        static char buffer[MAX_STRING_SIZE];

        int index;

 

        for(index = 0; string[index]; index++)

                buffer[index]= toupper(string[index]);

        buffer[index]= 0


        return buffer;

}

 

上面的函数是非可重入(也是非线程安全的)。运用之前介绍的第一种方法将函数改写为可重入函数,代码如下:

char *strtoupper(char *string)

{

        char *buffer;

        int index;

        

        buffer= malloc(MAX_STRING_SIZE);

        for(index = 0; string[index]; index++)

                buffer[index]= toupper(string[index]);

        buffer[index]= 0

 

        return buffer;

}

 

更佳的改写方式是改变函数的外部接口。调用者必须为输入和输出字符串提供存储空间,代码如下:

char *strtoupper_r(char *in_str, char *out_str)

{

        int index;

 

        for(index = 0; in_str[index]; index++)

        out_str[index]= toupper(in_str[index]);

        out_str[index]= 0

 

        return out_str;

}

 

非可重入的C标准库是按照第二种方法改写的。这一点会在后文提到。

 

       在连续的调用之间(由函数)保存信息

 

在连续的函数调用之间,不应该由函数保存任何信息,因为多个线程可能一个接一个的调用该函数。如果一个函数需要在连续的调用中保存某个信息,例如工作缓存区或是指针,这个信息应该由调用者负责保存

考虑下面的例子。lowercase_c函数在连续调用中返回字符串中字符的小写字符。与strtok()函数的使用方法类似,该字符串只在函数第一次调用时作为参数提供。函数在到达字符串尾部时返回值为0。函数的实现代码如下:


char lowercase_c(char *string)

{

        static char *buffer;

        static int index;

        char c = 0;


        if(string != NULL) {

                buffer= string;

                index= 0;

        }


        for(; c = buffer[index]; index++) {

                if(islower(c)) {

                        index++;

                        break;

                }

        }

        return c;

}

 

该函数是非可重入的。为了将其改写为可重入函数,由函数的静态变量index所保存的信息,应该改为由调用者负责保存。函数的可重入版本实现如下:

char reentrant_lowercase_c(char *string, int *p_index)

{

        charc = 0;


        for(; c = string[*p_index]; (*p_index)++) {

                if(islower(c)) {

                        (*p_index)++;

                        break;

                  }

        }

        return c;

}

 

函数的外部接口和使用方法都需要修改。调用者必须在每次调用函数时提供字符串参数,并且在第一次调用前将index变量初始化为0,正如以下代码所展示的:

char *my_string;

char my_char;

int my_index;

...

my_index = 0;

while (my_char = reentrant_lowercase_c(my_string,&my_index)) {

        ...

}

 

函数线程安全化

 

在多线程程序中,所有被多个线程调用的函数都要求是线程安全的。然而,有一种方法能够实现在多线程程序中调用非线程安全的函数。同样需要注意的是,非可重入的函数通常也是非线程安全的,然而将其改写为可重入后,同时也就变为线程安全的了。

 

       为共享资源加锁

 

使用静态数据或其他共享资源(如文件、终端)的函数,必须通过加锁的方式来将对资源的访问串行化来实现线程安全。例如,下面的函数是非线程安全的。

int increment_counter()

{

        staticint counter = 0;

 

        counter++;

        returncounter;

}

为了实现线程安全,需要用一个静态锁来限制对静态变量counter的访问,如下面的代码所示(伪代码)


int increment_counter();

{

        static int counter = 0;

        static lock_type counter_lock = LOCK_INITIALIZER;

 

        lock(counter_lock);

        counter++;

        unlock(counter_lock);

        return counter;

}

在使用线程库的多线程应用程序中,应该是用互斥锁来实现共享资源访问的串行化。独立的库有可能在线程之外的上下文环境中工作,因此,需要使用其他类型的锁。


使用非线程安全函数的解决方法


通过某种解决方法,非线程安全函数是可以被多个线程调用的。这在某些情况下或许是有用的,特别是当在多线程程序中使用一个非线程安全函数库的时候——或者是出于测试的目的,或者是由于没有相应的线程安全版本可用。这种解决方法会增加开销,因为它需要将对某个或一组函数的调用进行串行化。


   使用作用于整个函数库的锁,在每次使用该函数库(调用库中的某个函数或是访问库中的全局变量)时加锁,如下面的伪代码所示:


      lock(library_lock);

      library_call();

      unlock(library_lock);

      

      lock(library_lock);

      x= library_var;

      unlock(library_lock);


    该解决方法有可能会造成性能瓶颈,因为在任意时刻,只有一个线程能任意的访问或是用该库。只有在该库很少被使用的情况下,或是作为一种快速的实现方式,该方法才是可接受的。

 

    使用作用于单个库组件(函数或是全局变量)或是一组组件的锁,如下面的伪代码所示:

      

      lock(library_moduleA_lock);

      library_moduleA_call();

      unlock(library_moduleA_lock);

      

      lock(library_moduleB_lock);

      x= library_moduleB_var;

      unlock(library_moduleB_lock);

 

      这种方法与前者相比要复杂一些,但是能提高性能。

由于该类解决方式只应该在应用程序而不是函数库中使用,可以使用互斥锁(mutex)来为整个库加锁。

 

可重入和线程安全函数库

 

可重入和线程安全函数库,不仅在多线程环境,在并行以及异步编程的广泛领域中也是很有用的。因此,坚持使用和编写可重入和线程安全函数是一个很好的编程习惯。

 

使用函数库

 

AIX base OS附带函数库中有几个是线程安全的。目前的AIX版本中,以下函数库是线程安全的:

      

    *C标准函数库

    BSD兼容的函数库

 

某些C标准库函数是非可重入的,例如ctime()strtok()。这些函数的对应可重入版本的名字为原函数加_r后缀

在编写多线程程序时,应该使用可重入版本的库函数替代原始版本。例如,下面的代码:


token[0] = strtok(string, separators);

i = 0;

do 

{

        i++;

        token[i]= strtok(NULL, separators);

} while (token[i] != NULL);

 

在一个多线程程序中应该替换成下面的代码:

 

char *pointer;

...

token[0] = strtok_r(string, separators,&pointer);

i = 0;

do 

{

        i++;

        token[i]= strtok_r(NULL, separators, &pointer);

} while (token[i] != NULL);


非线程安全的函数库在程序中可以仅由一个线程使用。程序员必须保证使用该函数的线程的唯一性;否则,程序将会执行未期待的行为,甚至崩溃。


改写函数库


下面强调了将现存函数库改写为可重入和线程安全版本的主要步骤,只适用于C语言的函数库。

    识别出由函数库导出的所有全局变量。这些全局变量通常是在头文件中由export关键字定义的。

      导出的全局变量应该被封装起来。每个变量应该被设为函数库所私有的(通过static关键字实现),然后创建全局变量的访问函数来执行对全局变量的访问。


    识别出所有静态变量和其他共享资源。静态变量通常是由static关键字定义的。

         每个共享资源都应该与一个关联起来,锁的粒度(也就是锁的数量),影响着函数库的性能。为了初始化所有锁,可能需要一个仅被调用一次的初始化函数。


识别所有非可重入函数,并将其转化为可重入。参见函数可重入化


    识别所有非线程安全函数,并将其转化为线程安全。参见函数线程安全化。


对可重性和线程安全的小结

首先,可重入和线程安全是两个并不等同的概念,一个函数可以是可重入的,也可以是线程安全的,可以两者均满足,可以两者皆不满组(该描述严格的说存在漏洞,参见第二条)。

   其次,从集合和逻辑的角度看,可重入是线程安全的子集,可重入是线程安全的充分非必要条件。可重入的函数一定是线程安全的,反过来则不成立。

    第三,POSIX中对可重入和线程安全这两个概念的定义:
    
    ReentrantFunction:

    A function whose effect, when called by two or more threads, is guaranteed to be as if the threads each executed the function one after another in an undefined order, even if the actual execution is interleaved.

                                                                               From IEEEStd 1003.1-2001 (POSIX 1003.1)
                                                                                                     -- Base Definitions, Issue 6
    Thread-SafeFunction
     
    A function that may be safely invoked concurrently by multiplethreads.

   另外还有一个Async-Signal-Safe的概念

    Async-Signal-SafeFunction:
     
    A functionthat may be invoked, without restriction fromsignal-catchingfunctions. No function is async-signal -safe unless explicitlydescribed as such.

   以上三者的关系为:
    
    ReentrantFunction 必然是Thread-SafeFunctionAsync-Signal-SafeFunction
  
   
可重入与线程安全的区别体现在能否在signal处理函数中被调用的问题上,可重入函数在signal处理函数中可以被安全调用,因此同时也是Async-Signal-SafeFunction;而线程安全函数不保证可以在signal处理函数中被安全调用,如果通过设置信号阻塞集合等方法保证一个非可重入函数不被信号中断,那么它也是Async-Signal-SafeFunction。

    值得一提的是POSIX 1003.1的SystemInterface缺省是Thread-Safe的,但不是Async-Signal-Safe的。Async-Signal-Safe的需要明确表示,比如fork()和signal()。

最后让我们来构想一个线程安全但不可重入的函数:

  假设函数func()在执行过程中需要访问某个共享资源,因此为了实现线程安全,在使用该资源前加锁,在不需要资源时解锁。

  假设该函数在某次执行过程中,在已经获得资源锁之后,有异步信号发生,程序的执行流转交给对应的信号处理函数;再假设在该信号处理函数中也需要调用函数func(),那么func()在这次执行中仍会在访问共享资源前试图获得资源锁,然而我们知道前一个func()实例已然获得该锁,因此信号处理函数阻塞——另一方面,信号处理函数结束前被信号中断的线程是无法恢复执行的,当然也没有释放资源的机会,这样就出现了线程和信号处理函数之间的死锁局面。

   因此,func()尽管通过加锁的方式能保证线程安全,但是由于函数体对共享资源的访问,因此是非可重入。



  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值