本文是一篇译文,原文连接:http://www-01.ibm.com/support/knowledgecenter/ssw_aix_61/com.ibm.aix.genprogc/writing_reentrant_thread_safe_code.htm?cp=ssw_aix_61%2F13-3-12-18
在单线程程序中只存在一条控制流,因此单线程程序代码无需是可重入与线程安全的。而在多线程的程序中,同一个函数和资源则可能会被若干控制流同时访问。
为了保护资源的完整性,多线程程序的代码必须是可重入且线程安全的。
可重入与线程安全都与函数处理资源的方式有关,并且它们是两个独立的概念:一个函数可以是可重入的、或者线程安全的、或者既是可重入又是线程安全的。
本节包含了关于编写可重入与线程安全程序的内容,但并不涵盖编写高效线程的主题。高效线程程序是指高效的并行化程序,必须在程序的设计阶段就考虑到线程的效率问题。现有的单线程程序也可以改造为高效线程程序,但这需要对代码进行完全的重新设计与编写。
Reentrance
一个可重入的函数不能在连续的调用中持有同一个静态数据(not hold static data over successive calls),也不能返回指向静态数据的指针。所有的数据都应该由函数调用者提供。可重入函数中不能够调用非可重入函数。
一个非可重入的函数常常(但不总是)可以通过它的外部接口和使用方法被识别出来。例如,“strtok”函数就是非可重入的,因为它持有了待分割的字符串。“ctime“函数也不是可重入的,它返回了一个指向静态数据的指针,而这个指针在每次调用时都会被覆盖。
Thread safety
一个线程安全的函数通过“锁“来保护对共享资源的并发访问。线程安全只与函数的实现有关,而不影响它的外部接口。
在C语言中,本地变量是在栈上动态分配的。因此,任何不使用静态数据和其他共享资源的函数都是线程安全的,如下面的例子所示:
/* threadsafe function */ int diff(int x, int y) { int delta; delta = y - x; if (delta < 0) delta = -delta; return delta; }
另外,使用全局数据(global data)是非线程安全的。全局数据应该由每个线程来维护或封装,并因此能够被顺序访问。一个线程可以读到与其他线程的错误相关的错误码。在AIX中,每个线程都有自己的errno值。
Making a function reentrant
在大多数情况下,将非可重入函数替换为可重入函数需要改变接口,而改变接口就是为了实现可重入性。非可重入函数不能用在多线程程序中。更进一步来说,无法使一个非可重入函数变为线程安全的。
Returning data
许多非可重入函数返回一个指向静态数据的指针,这可以通过下面的几种方式来避免:
返回一个“动态分配的数据“。在这种情况下,由调用者负责进行存储空间的释放工作。这样做的好处是无需修改接口,但是无法保证向后兼容性。现有的单线程程序直接使用这种新函数而不做修改的话会引发内存泄漏的问题;
使用“由调用者提供的存储区“。尽管需要对接口进行修改,但还是推荐使用这种方法;
例如,一个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标准库就是采用了这种“由调用者提供存储区的方式“将非可重入的函数改写为可重入的。
Keeping data over successive calls
没有数据应该在相继的函数调用之间被保存,因为不同的线程可能会先后来调用这个函数。如果一个函数必须在相继的调用间保存一些数据,例如一个工作缓冲区或者指针,那么这些数据必须由调用者来提供。
考虑下面的例子。A函数返回一个字符串中连续的小写字符,字符串只在第一次调用的时候提供,就像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)) { ... }
Making a function threadsafe
在多线程的程序中,所有会被多个线程调用的函数都应该是线程安全的。然而,也存在一种变通的方法在多线程程序中使用非线程安全的函数。非可重入的函数通常都不是线程安全的,但把它们改写为可重入函数后常常也会令它们成为线程安全的。
Locking shared resources
使用静态数据或其他任何共享资源(例如,文件和终端)的函数,为了做到线程安全,都需要通过“锁”机制来实现对这些资源的顺序化访问。例如,下面的函数不是线程安全的:
/* thread-unsafe function */ int increment_counter() { static int counter = 0; counter++; return counter; }
为了做到线程安全,静态变量counter应该通过一个“静态锁(static lock)“被保护起来,如下:
/* pseudo-code threadsafe function */ int increment_counter(); { static int counter = 0; static lock_type counter_lock = LOCK_INITIALIZER; pthread_mutex_lock(counter_lock); counter++; pthread_mutex_unlock(counter_lock); return counter; }
在一个使用线程库的多线程应用程序中,应该使用互斥锁来实现资源的顺序访问。独立的库则可能需要在线程上下文之外工作,那么就使用其他类型的锁。
Workarounds for thread-unsafe functions
可以通过一种变通的方法在多线程程序中调用非线程安全的函数。这种方法是有用的,尤其是在多线程程序中使用非线程安全的库时(为了测试或正在等待线程安全的库版本时)。这些变通带来了一些额外的开销,因为它包括对全部函数甚至函数组进行序列化。下面是一些可能的变通方法:
为这个库使用一把全局的锁,并在每次使用库的时候锁住它(调用库函数或使用库全局变量)。这个方案会带来性能瓶颈,因为给定时刻只有一个线程能够访问库的任一部分。下面的伪代码给出的方案只有在库极少被访问时才是可接受的,或者是作为一种初步的、快速的变通方案;
/* 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);
Reentrant and threadsafe libraries
可重入和线程安全的库被广泛使用在并行(和异步)程序环境中而不仅仅是线程中。总是编写可重用和线程安全的函数是一种好的编码实践。
Using libraries
一些由AIX Base Operating System提供的库是线程安全的。在当前的AIX版本中,下列库是线程安全的:
Standard C library(libc.a)
Berkeley compatibility library(libbsd.a)
有些标准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);
非线程安全的库只能在单线程程序中使用。要确保只有一个线程在使用库,否则,程序可能会出现不可预期的行为,甚至停止运行。
Converting libraries
当要将一个现有的库转换为可重入与线程安全的库时,考虑以下几个方面,这些内容只适用于C语言库:
识别导出的全局变量。这些变量通常定义在一个头文件中,并伴有export关键字。导出的全局变量应该被封装起来。这些变量应该被私有化(在库函数源代码中通过static关键字定义),并创建访问函数(读和写);
识别静态变量和其他共享资源。静态变量通常通过static关键字定义。锁应该伴随所有的共享资源。锁的粒度决定了锁的数量,并进而影响到库的性能。可以使用一次性初始化方式来对锁进行初始化;
识别非可重入函数并把它们可重入化。更多信息参见 Making a Function Reentrant;
识别非线程安全函数并把它们线程安全化。更多信息参见 Making a Function threadsafe;