编写可重入和线程安全的代码(Writing Reentrant and Thread-Safe 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
转载时务必以超链接形式标明文章原始出处及作者、译者信息。
在单线程程序中,只有单一控制流,程序所执行的代码不必是可重入或线程安全的。在多线程程序中,同一函数和同一资源有可能被多个控制流并发访问。为了保证资源的完整性,多线程程序中所使用的代码必须是可重入和线程安全的。
本节提供了编写可重入和线程安全程序的相关信息。然而本节的主题并不是如何编写高效并行化的多线程程序,这只有在程序设计阶段才能完成。现有的单线程程序必须彻底的重新设计和重新编写,才能实现高效线程化。
理解可重入与线程安全
可重入与线程安全这两个概念,都与函数处理资源的方式有关。可重入与线程安全是两个独立的概念,一个函数可以是可重入或是线程安全,或是同时满足两者,或是同时不满足两者的。
可重入
一个可重入的函数在执行中并不使用静态数据,也不返回指向静态数据的指针。所有使用到的数据都由函数的调用者提供。可重入函数在函数体内不能调用非可重入函数。
一个非可重入函数通常(尽管不是所有情况下)由它的外部接口和使用方法即可进行判断。例如 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关键字定义的。
每个共享资源都应该与一个锁关联起来,锁的粒度(也就是锁的数量),影响着函数库的性能。为了初始化所有锁,可能需要一个仅被调用一次的初始化函数。
* 识别所有非可重入函数,并将其转化为可重入。参见函数可重入化