编写可重入和线程安全的代码
在单线程的进程中,有且仅有一个控制流。这种进程执行的代码不必是可重入的,或线程安全的。在多线程的程序中,同一个函数或是同一个资源可能被多个控制流并发地访问。为了保证资源的完整性,为多线程程序写的代码必须是可重入的和线程安全的。
本节提供了一些编写可重入和线程安全的代码的信息。
理解可重入和线程安全
可重入和线程安全都是指函数处理资源的方式。可重入和线程安全是两个相互独立的概念,一个函数可以仅是可重入的,可以仅是线程安全的,可以两者都是,可以两者都不是。
可重入
一个可重入的函数不能为后续的调用保持静态(或全局)数据,也不能返回指向静态(或全局)数据的指针。函数中用到的所有的数据,都应该由该函数的调用者提供(不包括栈上的局部数据)。一个可重入的函数不能调用不可重入的函数。
一个不可重入的函数经常可以(但不总是可以)通过它的外部接口和功能识别出来。例如strtok是不可重入的,因为它保存着将被识别为token的字符串。ctime也不是一个可重入的函数,它会返回一个指向静态数据的指针,每次调用都可能覆盖这些数据。
线程安全
一个线程安全的函数通过“锁”来保护共享的资源不被并发地访问。“线程安全”仅关心函数的实现,而不影响它的外部接口。
在C中,局部变量是在栈上动态分配的,因此,任何一个不使用静态数据和其它共享资源的函数就是最平凡的线程安全的。例如,下面这个函数就是线程安全的:
/* thread-safe function */
int diff(int x,int y)
{
int delta;
delta=y-x;
if(delta<0)
delta=-delta;
return delta;
}
对全局变量的使用是线程“不安全”的。应该为每一个线程维护一份拷贝,或者将其封装起来,使得对它的访问变成串行的。
使一个函数变成可重入的
在大部分情况下,不可重入的函数修改成可重入的函数时,需要修改函数的对外接口。不可重入的函数不能被多线程的程序调用。一个不可重入的函数,基本上不可能是线程安全的。
返回值
很多不可重入的函数返回一个指向静态数据的指针。这个问题可以有两种解决办法:
1、返回从堆中分配的空间的地址。在这种情况下,调用者必须负责释放堆中的空间。这种办法的优点是不必修改函数的外部接口,但是不能向后兼容。现存的单线程的程序使用修改后的函数会导致内存泄露(因为它们没有释放空间)。
2、由调用者提供空间。尽管函数的外部接口需要改变,仍然推荐这种方法。
例如,一个strtoupper函数(个人感觉这个东东写得很儍)一个字符串转换成大写,实现为:
/* non-reentrant function */
char *srttoupper(char *string)
{
static char buffer[MAX_STR_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;
buffer=malloc(MAX_STR_SIZE);
/*error check should be checked*/
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_string)
{
int index;
for(index=0;in_str[index];index++)
out_str[index]=toupper(in_str[index]);
out_str[index]=0;
return out_str;
}
为后继的调用保持数据
一个可重入的函数不应该为后续的调用保持数据(即后继的调用和本次调用无关),因为下一次调用可能是由不同的线程调用的。如果一个函数需要在连续的调用之间维护一些数据,例如一个工作缓冲区或是一个指针,这些数据(资源)应该由调用这个函数的函数提供。
例如:返回指定字符串中的下一个小写字符的函数。字符串只有在第一次调用时被提供,就像strtok一样。到达末尾时返回0(我认为这个函数写得有问题,姑且明白大意就好):
/* non-reentrant function*/
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;
}
该函数是不可重入的。要使它改写成可重入的,其中的静态数据应该由它的调用者维护。
/* reentrant function*/
char reentrant_lowercase_c(char *string,int *p_index)
{
char c=0;
for(;c=string[*p_index];(*p_index)++){
if(islower(c)){
(*p_index)++;
break;
}
}
return c;
}
函数的对外接口和使用方法均改变了。
使一个函数变成线程安全的
在一个多线程的程序中,所有的被多线程调用的函数多必须是线程安全的(或可重入的)。注意,不可重入的函数一般都是线程“不安全”的,然而,将它们改写成可重入的同时,一般就会将它们变成线程安全的。
“锁”住共享资源
使用静态数据或者其它任何共享资源(如文件、终端等)的函数,必须对这些资源加“锁”以实现对它们的串行访问,这样才能成为线程安全的函数。例如:
/* thread-unsafe function*/
int increament_counter()
{
static int counter=0;
counter++;
return 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;
}
在一个使用线程库的多线程程序中,应该使用信号量来串行化共享资源的访问,或者其它“锁”。
综上所述:
1、可重入:多个线程调用是不会互相影响。例如第一个lowercase_c函数是不可重入的,在另一个线程中第二次调用它时,必将覆盖掉第一个线程中的设定的字符串。
2、线程安全:解决多个线程共享资源的问题
阅读(1181) | 评论(0) | 转发(0) |