(转)编写可重入和线程安全的代码(Writing Reentrant and ThreadSafe Code)

Writing Reentrant and Thread-Safe Code

原文地址: http:/unet.univie.ac.at/aix/aixprggd/genprogc/writing_reentrant_thread_safe_code.htm

译者:Love. Katherine,2007-03-28

译文地址:http://blog.csdn.net/lovekatherine/archive/2007/03/28/1544585.aspx

转载时务必以超链接形式标明文章原始出处及作者、译者信息。

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

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

理解可重入与线程安全

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

可重入

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

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

线程安全

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

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

/* thread-safe function */

int diff(int x, int y)

{

        int delta;

        delta = y - x;

        if (delta < 0)

                delta = -delta;

        return delta;

}

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

函数可重入化

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

       返回数据

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

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

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

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

/* non-reentrant function */

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;

}

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

/* reentrant function (a poor solution) */

char *strtoupper(char *string)

{

        char *buffer;

        int index;

        /* error-checking should be performed! */

        buffer = malloc(MAX_STRING_SIZE);

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

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

        buffer[index] = 0

        return buffer;

}

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

/* reentrant function (a better solution) */

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。函数的实现代码如下:

/* non-reentrant function */

char lowercase_c(char *string)

{

        static char *buffer;

        static int index;

        char c = 0;

        /* stores the string on first call */

        if (string != NULL) {

                buffer = string;

                index = 0;

        }

        /* searches a lowercase character */

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

                if (islower(c)) {

                        index++;

                        break;

                }

        }

        return c;

}

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

/* reentrant function */

char reentrant_lowercase_c(char *string, int *p_index)

{

        char c = 0;

        /* no initialization - the caller should have done it */

        /* searches a lowercase character */

        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)) {

        ...

}

函数线程安全化

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

       为共享资源加锁

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

/* thread-unsafe function */

int increment_counter()

{

        static int counter = 0;

        counter++;

        return counter;

}

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

/* pseudo-code thread-safe function */

int increment_counter();

{

        static int counter = 0;

        static lock_type counter_lock = LOCK_INITIALIZER;

        lock(counter_lock);

        counter++;

        unlock(counter_lock);

        return counter;

}

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

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

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

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

      /* this is pseudo-code! */

      

      lock(library_lock);

      library_call();

      unlock(library_lock);

      

      lock(library_lock);

      x = library_var;

      unlock(library_lock);

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

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

      /* this is pseudo-code! */

      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关键字定义的。

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

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

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

线程安全函数
• 概念:
       线程安全的概念比较直观。一般说来,一个函数被称为线程安全的,当且仅当被多个并发线程反复调用时,它会一直产生正确的结果。
• 确保线程安全:
       要确保函数线程安全,主要需要考虑的是线程之间的共享变量。属于同一进程的不同线程会共享进程内存空间中的全局区和堆,而私有的线程空间则主要包括栈和寄 存器。因此,对于同一进程的不同线程来说,每个线程的局部变量都是私有的,而全局变量、局部静态变量、分配于堆的变量都是共享的。在对这些共享变量进行访 问时,如果要保证线程安全,则必须通过加锁的方式。
• 线程不安全的后果:
       线程不安全可能导致的后果是显而易见的——共享变量的值由于不同线程的访问,可能发生不可预料的变化,进而导致程序的错误,甚至崩溃。

可重入函数
• 概念:
       可重入的概念基本没有比较正式的完整解释,多数的文档都只是说明什么样的情况才能保证函数可重入,但没有完整定义。按照Wiki上的说法,“A computer program or routine is described as reentrant if it can be safely executed concurrently; that is, the routine can be re-entered while it is already running.”根据笔者的经验,所谓“重入”,常见的情况是,程序执行到某个函数foo()时,收到信号,于是暂停目前正在执行的函数,转到信号处理 函数,而这个信号处理函数的执行过程中,又恰恰也会进入到刚刚执行的函数foo(),这样便发生了所谓的重入。此时如果foo()能够正确的运行,而且处 理完成后,之前暂停的foo()也能够正确运行,则说明它是可重入的。
• 确保可重入:
       要确保函数可重入,需满足以下几个条件:
       1、不在函数内部使用静态或全局数据
       2、不返回静态或全局数据,所有数据都由函数的调用者提供。
       3、使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据。
       4、不调用不可重入函数。
• 不可重入的后果:
       不可重入的后果主要体现在象信号处理函数这样需要重入的情况中。如果信号处理函数中使用了不可重入的函数,则可能导致程序的错误甚至崩溃。

可重入与线程安全

     首先,可重入和线程安全是两个并不等同的概念,一个函数可以是可重入的,也可以是线程安全的,可以两者均满足,可以两者皆不满组(该描述严格的说存在漏洞,参见第二条)。
    其次,从集合和逻辑的角度看,可重入是线程安全的子集,可重入是线程安全的充分非必要条件。可重入的函数一定是线程安全的,然过来则不成立。
    第三,POSIX 中对可重入和线程安全这两个概念的定义:
Reentrant Function:
    A function whose effect, when called by two or more threads,is guaranteed to be as if the threads each executed thefunction one after another in an undefined order, even ifthe actual execution is interleaved.
                                                                                                        From IEEE Std 1003.1-2001 (POSIX 1003.1)
                                                                                                                                      -- Base Definitions, Issue 6
Thread-Safe Function:
    A function that may be safely invoked concurrently by multiple threads.
   另外还有一个 Async-Signal-Safe的概念
Async-Signal-Safe Function:
    A function that may be invoked, without restriction fromsignal-catching functions. No function is async-signal -safe unless explicitly described as such.
    以上三者的关系为:
Reentrant Function 必然是Thread-Safe Function和Async-Signal-Safe Function
可 重入与线程安全的区别体现在能否在signal处理函数中被调用的问题上,可重入函数在signal处理函数中可以被安全调用,因此同时也是Async- Signal-Safe Function;而线程安全函数不保证可以在signal处理函数中被安全调用,如果通过设置信号阻塞集合等方法保证一个非可重入函数不被信号中断,那 么它也是Async-Signal-Safe Function。
     值得一提的是POSIX 1003.1的System Interface缺省是Thread-Safe的,但不是Async-Signal-Safe的。Async-Signal-Safe的需要明确表示,比如fork ()和signal()。
最后让我们来构想一个线程安全但不可重入的函数:
   假设函数func()在执行过程中需要访问某个共享资源,因此为了实现线程安全,在使用该资源前加锁,在不需要资源解锁。
   假设该函数在某次执行过程中,在已经获得资源锁之后,有异步信号发生,程序的执行流转交给对应的信号处理函数;再假设在该信号处理函数中也需要调用函数 func(),那么func()在这次执行中仍会在访问共享资源前试图获得资源锁,然而我们知道前一个func()实例已然获得该锁,因此信号处理函数阻 塞——另一方面,信号处理函数结束前被信号中断的线程是无法恢复执行的,当然也没有释放资源的机会,这样就出现了线程和信号处理函数之间的死锁局面。
    因此,func()尽管通过加锁的方式能保证线程安全,但是由于函数体对共享资源的访问,因此是非可重入。

       - 如果一个函数中用到了全局或静态变量,那么它不是线程安全的,也不是可重入的;
       - 如果我们对它加以改进,在访问全局或静态变量时使用互斥量或信号量等方式加锁,则可以使它变成线程安全的,但此时它仍然是不可重入的,因为通常加锁方式是针对不同线程的访问,而对同一线程可能出现问题;
       - 如果将函数中的全局或静态变量去掉,改成函数参数等其他形式,则有可能使函数变成既线程安全,又可重入。

       比如:strtok函数是既不可重入的,也不是线程安全的;加锁的strtok不是可重入的,但线程安全;而strtok_r既是可重入的,也是线程安全的。

原文地址:https://www.cnblogs.com/yysblog/p/2550754.html