线程在遇到重入问题时与信号处理程序是类似的,在这两种情况下,多个控制线程在相同的时间有可能调用相同的函数。如果一个函数在相同的时间点可以被多个线程安全地调用,就称该函数是线程安全的。在 Single UNIX Specification 定义的函数中,下面这些是[color=red]不能[/color]保证线程安全的(另外,ctermid 和 tmpnam 传入空指针参数时也不能保证线程安全)。
[img]http://dl2.iteye.com/upload/attachment/0127/9311/57e9cf20-12a5-3f92-94d1-b968b8f8a58b.png[/img]
支持线程安全函数的操作系统会在<unistd.h>中定义符号_POSIX_THREAD_SAFE_FUNCTIONS。应用程序也可以在 sysconf 函数中传入 _SC_PTHREAD_SAFE_FUNCTIONS 参数在运行时检查是否支持线程安全函数。操作系统实现支持线程安全函数这个特性时,对 POSIX.1 中的一些非线程安全函数,它会提供可替代的线程安全版本。下表列出了这些函数的线程安全版本,它们在原来的名字后面加了“_r”,表明这些版本是可重入的(很多函数并不是线程安全的,因为它们返回的数据存放在静态的内存缓冲区中。通过修改接口,要求调用者自己提供缓冲区可以使函数变为线程安全)。
[img]http://dl2.iteye.com/upload/attachment/0127/9313/65f72ada-77f5-3788-9ca0-6adb2b7e6357.png[/img]
如果一个函数对多个线程来说是可重入的,则该函数是线程安全的,但这并不能说明对信号处理程序来说也是可重入的。如果函数对异步信号处理程序的重入是安全的,则可以说函数是异步信号安全的,在[url=http://aisxyz.iteye.com/admin/blogs/2394900]信号默认处理动作及可重入函数[/url]一节中提到的可重入函数就是异步信号安全的。
下面给出了一个 getenv 的可重入版本 getenv_r,它使用 pthread_once 函数来确保不管多少线程同时竞争调用 getenv_t,每个进程都只调用 thread_init 函数一次。
这里为了使该函数可重入而改变了其接口,调用者需要提供自己的缓冲区。而为了保证线程安全,使用了互斥量来保护环境在搜索请求的字符时不被修改。不仅如此,这里使用的还是可递归的互斥量,以保证它对信号处理程序也是可重入的。如果使用的是非递归的互斥量,则线程从信号处理程序中再用到该互斥量时就有可能出现死锁,因为在 getenv_r 中可能已经对其加锁了。
除了上表中的函数,POSIX.1 还提供了以线程安全的方式管理 FILE 对象的方法。可以使用 flockfile 和 ftrylockfile 获取给定 FILE 对象关联的锁,这个锁是递归的,这表示当占有这把锁的时候还可以再次获取该锁。
如果标准 I/O 例程都获取它们各自的锁,那么在做一次一个字符的 I/O 时就会出现严重的性能下降,因为这需要对每一个字符的读写操作进行获取锁和释放锁的动作。为避免这种开销,出现了如下不加锁版本的基于字符的标准 I/O 例程(不过除非被上面的 flockfile 类函数的调用包围,否则尽量不要使用,因为它们会导致不可预期的结果,比如由于多个控制线程非同步访问数据引起的种种问题)。
一旦对 FILE 对象进行加锁,就可以在释放锁前对这些函数进行多次调用,这样就可以在多次的数据读写上分摊总的加解锁的开销。
[img]http://dl2.iteye.com/upload/attachment/0127/9311/57e9cf20-12a5-3f92-94d1-b968b8f8a58b.png[/img]
支持线程安全函数的操作系统会在<unistd.h>中定义符号_POSIX_THREAD_SAFE_FUNCTIONS。应用程序也可以在 sysconf 函数中传入 _SC_PTHREAD_SAFE_FUNCTIONS 参数在运行时检查是否支持线程安全函数。操作系统实现支持线程安全函数这个特性时,对 POSIX.1 中的一些非线程安全函数,它会提供可替代的线程安全版本。下表列出了这些函数的线程安全版本,它们在原来的名字后面加了“_r”,表明这些版本是可重入的(很多函数并不是线程安全的,因为它们返回的数据存放在静态的内存缓冲区中。通过修改接口,要求调用者自己提供缓冲区可以使函数变为线程安全)。
[img]http://dl2.iteye.com/upload/attachment/0127/9313/65f72ada-77f5-3788-9ca0-6adb2b7e6357.png[/img]
如果一个函数对多个线程来说是可重入的,则该函数是线程安全的,但这并不能说明对信号处理程序来说也是可重入的。如果函数对异步信号处理程序的重入是安全的,则可以说函数是异步信号安全的,在[url=http://aisxyz.iteye.com/admin/blogs/2394900]信号默认处理动作及可重入函数[/url]一节中提到的可重入函数就是异步信号安全的。
下面给出了一个 getenv 的可重入版本 getenv_r,它使用 pthread_once 函数来确保不管多少线程同时竞争调用 getenv_t,每个进程都只调用 thread_init 函数一次。
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <pthread.h>
pthread_mutex_t env_mutex;
static pthread_once_t initflag = PTHREAD_ONCE_INIT;
static void thread_init(void){
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
pthread_mutex_init(&env_mutex, &attr);
pthread_mutexattr_destroy(&attr);
}
extern char **environ;
int getenv_r(const char *name, char buf[], int buflen){
// buf[0] = '\0';
int len = strlen(name);
pthread_once(&initflag, thread_init);
pthread_mutex_lock(&env_mutex);
int i = 0;
for(; environ[i] != NULL; i++){
if(strncmp(name, environ[i], len)==0 && environ[i][len]=='='){
if(strlen(&environ[i][len+1]) < buflen){
strcpy(buf, &environ[i][len+1]);
pthread_mutex_unlock(&env_mutex);
return 0;
}else{
pthread_mutex_unlock(&env_mutex);
return ENOSPC;
}
}
}
pthread_mutex_unlock(&env_mutex);
return ENOENT;
}
这里为了使该函数可重入而改变了其接口,调用者需要提供自己的缓冲区。而为了保证线程安全,使用了互斥量来保护环境在搜索请求的字符时不被修改。不仅如此,这里使用的还是可递归的互斥量,以保证它对信号处理程序也是可重入的。如果使用的是非递归的互斥量,则线程从信号处理程序中再用到该互斥量时就有可能出现死锁,因为在 getenv_r 中可能已经对其加锁了。
除了上表中的函数,POSIX.1 还提供了以线程安全的方式管理 FILE 对象的方法。可以使用 flockfile 和 ftrylockfile 获取给定 FILE 对象关联的锁,这个锁是递归的,这表示当占有这把锁的时候还可以再次获取该锁。
#include <stdio.h>
int ftrylockfile(FILE *fp);
/* 返回值:若成功,返回 0;若不能获取锁,返回非 0 数值 */
void flockfile(FILE *fp);
void funlockfile(FILE *fp);
如果标准 I/O 例程都获取它们各自的锁,那么在做一次一个字符的 I/O 时就会出现严重的性能下降,因为这需要对每一个字符的读写操作进行获取锁和释放锁的动作。为避免这种开销,出现了如下不加锁版本的基于字符的标准 I/O 例程(不过除非被上面的 flockfile 类函数的调用包围,否则尽量不要使用,因为它们会导致不可预期的结果,比如由于多个控制线程非同步访问数据引起的种种问题)。
#include <stdio.h>
int getchar_unlocked(void);
int getc_unlocked(FILE *fp);
/* 两个函数的返回值:若成功,返回下一个字符;若遇到文件尾或者出错,返回 EOF */
int putchar_unlocked(int c);
int putc_unlocked(int c, FILE *fp);
/* 两个函数的返回值:若成功,返回 c;否则,返回 EOF */
一旦对 FILE 对象进行加锁,就可以在释放锁前对这些函数进行多次调用,这样就可以在多次的数据读写上分摊总的加解锁的开销。
本文介绍了线程安全的概念及其在操作系统中的实现方式,包括如何通过互斥量等手段保证函数的线程安全性和可重入性,并列举了一些常见的线程安全函数及其线程安全版本。
252

被折叠的 条评论
为什么被折叠?



