原文地址:https://www.ibm.com/support/knowledgecenter/zh/ssw_aix_61/com.ibm.aix.genprogc/writing_reentrant_thread_safe_code.htm
在单线程进程中,只存在一个控制流。因此,这些进程所执行的代码无需重入或是线程安全的。在多线程程序中,相同的功能和资源可以通过多个控制流并发访问。
要保护资源的完整性,编写的多线程程序代码必须能重入并是线程安全的。
重入和线程安全都与函数处理资源的方式相关。重入和线程安全是不同的概念:函数可以重入和/或线程安全化,或者两者都不可行。
此部分提供了有关编写重入和线程安全程序的信息。其中不涉及有关编写高效线程程序的主题。高效线程程序是高效率的并行化程序。您必须在设计程序的时候考虑到线程的效率。现有的单线程程序可以成为高效线程程序,但是这需要将这些程序完全重新设计和重新编写。
重入
重入函数不在连续的调用中保存静态数据,也不返回指向静态数据的指针。所有的数据都是由函数的调用程序提供的。重入函数不得调用非重入函数。
一般情况下,非重入函数是由其外部接口和用法标识的,但也并不总是这样。例如,strtok 子例程不是一个重入函数,因为它保存了将分割为多个标记的字符串。ctime 子例程同样不是重入函数;它返回了被每个调用所覆盖的静态数据的指针。
线程安全
线程安全函数通过锁来保护并发访问中的共享资源。线程安全只与函数的实现有关,并不会影响它的外部接口。
/* threadsafe function */
int diff(int x, int y)
{
int delta;
delta = y - x;
if (delta < 0)
delta = -delta;
return delta;
}
全局数据的使用是线程不安全的。全局数据应针对每个线程保存或被封装起来,这样可以使它的访问串行化。线程可以读取对应于由另一个线程引起的错误的错误代码。在 AIX® 中,每个线程都有自己的 errno 值。
使函数成为重入函数
在多数情况下,必须用带有已修改的将要重入的函数来替代非重入函数。非重入函数不能由多个线程使用。此外,可能也无法使非重入函数变为线程安全。
返回数据
- 返回动态分配的数据。在这种情况下,调用程序将负责释放存储量。好处在于无需对接口进行修改。但是,向后兼容性就无法保证了;现有的使用已修改函数的单线程程序在不更改的情况下不会释放存储量,这将导致内存泄漏。
- 使用调用程序提供的存储量。虽然必须修改接口,但是推荐使用这种方法。
/* 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 库子例程重入。
在连续调用中保存数据
在连续调用中将不保存任何数据,因为不同的线程可能连续地调用该函数。如果函数必须在连续调用中保存某些数据,比如工作缓存或指针,那么调用程序应提供该数据。
/* 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;
}
h
/* 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;
}
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;
}
/* 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;
}
在使用线程库的多线程应用程序中,应使用 mutex 来对共享资源进行串行化。独立的库可能需要在线程的上下文以外工作,因此,请使用其他种类的锁。
线程不安全函数的变通方法
- 对库使用全局锁定,并在每次使用该库的时候锁定它(通过调用一个库例程或使用一个库全局变量)。该解决方案可以会产生性能瓶颈,由于在任意给定的时间内只有一个线程能够对库的所有部分进行访问。以下伪码的解决方案只有在库很少被访问,或是作为一个初始的、快捷的实现变通方法时才是可接受的。
/* this is pseudo code! */ lock(library_lock); library_call(); unlock(library_lock); lock(library_lock); x = library_var; unlock(library_lock);
- 对每个库组件或组件组使用锁(例程或全局变量)。此种解决方案实现起来比上面的例子略微复杂一些,但是它可以改善性能。因为该变通方法只能在应用程序中使用,而不能在库中使用,所以可以使用 mutex 来锁定库。
/* 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);
重入和线程安全库
重入库和线程安全库并不仅仅在线程中有用,而且在大范围的并行(和异步)编程环境中也很有用。一直使用和编写重入函数和线程安全函数是很好的编程实践。
使用库
- 标准 C 库 (libc.a)
- Berkeley 兼容性库 (libbsd.a)
有些标准 C 子例程是非重入的,比如 ctime 和 strtok 子例程。这些重入版本的子例程的名称为原来的子例程名称上加上后缀 _r(下划线后面跟字母 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);
在一个程序中,线程不安全的库可能只由一个线程使用。请确保使用该库的线程的唯一性;否则,程序可出现意外的行为,甚至可能停止。
转换库
- 确定已导出的全局变量。那些变量通常是用 export 关键字在头文件中定义的。应将已导出的全局变量封装起来。变量应设为专用变量(使用 static 关键字在库源代码中定义),然后应创建访问(读和写)子例程。
- 确定静态变量和其他的共享资源。静态变量通常是用 static 关键字定义的。锁应该与任意的共享资源关联。锁定的详细程度,这样对锁数目的选择将影响到库的性能。可使用一次性初始化工具来初始化这些锁。
- 确定非重入函数并使之变为重入函数。有关更多信息,请参阅使函数成为重入函数。
- 确定线程不安全函数并使之成为线程安全函数。有关更多信息,请参阅使函数成为线程安全函数。