10.6节讨论了可重入函数和信号处理程序。线程在遇到重入问题时与信号处理程序是类似的。在这两种情况下,多个控制线程在相同的时间有可能调用相同的函数。
如果一个函数在相同的时间点可以被多个线程安全地调用,就称该函数是线程安全的。在Single UNIX Specification中定义的所有函数中,除了图12-9中列出的函数,其他函数都保证是线程安全的。另外,ctermid
和tmpnam
函数在参数传入空指针时并不能保证是线程安全的。类似地,如果参数mbstate_t
传入的是空指针,也不能保证wcrtomb
和wcsrtombs
函数是线程安全的。
支持线程安全函数的操作系统实现会在<unistd.h>
中定义符号_POSIX_THREAD_SAFE_FUNCTIONS
。
应用程序也可以在sysconf
函数中传入_SC_THREAD_SAFE_FUNCTIONS
参数在运行时检查是否支持线程安全函数。在SUSv4之前,要求所有遵循XSI的实现都必须支持线程安全函数,但是在SUSv4中,线程安全函数支持这个需求已经要求具体实现考虑遵循POSIX。
操作系统实现支持线程安全函数这个特性时,对POSIX.1中的一些非线程安全函数,它会提供可替代的线程安全版本。图12-10列出了这些函数的线程安全版本。这些函数的命名方式与它们的非线程安全版本的名字相似,只不过在名字最后加了_r
,表明这些版本是可重入的。很多函数并不是线程安全的,因为它们返回的数据存放在静态的内存缓冲区中。通过修改接口,要求调用者自己提供缓冲区可以使函数变为线程安全。
图12-9 POSIX.1中不能保证线程安全的函数
图12-10 替代的线程安全函数
如果一个函数对多个线程来说是可重入的,就说这个函数就是线程安全的。但这并不能说明对信号处理程序来说该函数也是可重入的。如果函数对异步信号处理程序的重入是安全的,那么就可以说函数是异步信号安全的。我们在10.6节中讨论可重入函数时,图10-4中的函数就是异步信号安全函数。
除了图12-10中列出的函数,POSIX.1还提供了以线程安全的方式管理FILE
对象的方法。可以使用flockfile
和ftrylockfile
获取给定FILE
对象关联的锁。这个锁是递归的:当你占有这把锁的时候,还是可以再次获取该锁,而且不会导致死锁。虽然这种锁的具体实现并无规定,但要求所有操作FILE
对象的标准I/O 例程的动作行为必须看起来就像它们内部调用了flockfile
和funlockfile
。
#include <stdio.h>
int ftrylockfile(FILE *fp);
//返回值:若成功,返回0;若不能获取锁,返回非0数值
void flockfile(FILE *fp);
void funlockfile(FILE *fp);
虽然标准的I/O例程可能从它们各自的内部数据结构的角度出发,是以线程安全的方式实现的,但有时把锁开放给应用程序也是非常有用的。这允许应用程序把多个对标准I/O函数的调用组合成原子序列。当然,在处理多个FILE
对象时,需要注意潜在的死锁,需要对所有的锁仔细地排序。
如果标准I/O例程都获取它们各自的锁,那么在做一次一个字符的I/O时就会出现严重的性能下降。在这种情况下,需要对每一个字符的读写操作进行获取锁和释放锁的动作。为了避免这种开销,出现了不加锁版本的基于字符的标准I/O例程。
#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);
除非被flockfile
(或ftrylockfile
)和funlockfile
的调用包围,否则尽量不要调用这4个函数,因为它们会导致不可预期的结果(比如,由于多个控制线程非同步访问数据引起的种种问题)。
一旦对FILE
对象进行加锁,就可以在释放锁之前对这些函数进行多次调用。这样就可以在多次的数据读写上分摊总的加解锁的开销。
实例
图12-11显示了getenv
(见7.9节)的一个可能实现。这个版本不是可重入的。如果两个线程同时调用这个函数,就会看到不一致的结果,因为所有调用getenv
的线程返回的字符串都存储在同一个静态缓冲区中。
#include <limits.h>
#include <string.h>
#define MAXSTRINGSZ 4096
static char envbuf[MAXSTRINGSZ];
extern char **environ;
char *
getenv(const char *name)
{
int i, len;
len = strlen(name);
for (i = 0; environ[i] != NULL; i++) {
if ((strncmp(name, environ[i], len) == 0) && (environ[i][len] == '=')) {
strncpy(envbuf, &environ[i][len+1], MAXSTRINGSZ-1);
return(envbuf);
}
}
return(NULL);
}
图12-11 getenv
的非可重入版本
图12-12给出了getenv
的可重入的版本。这个版本叫做getenv_r
。它使用pthread_once
函数来确保不管多少线程同时竞争调用getenv_r
,每个进程只调用thread_init
函数一次。12.6节会详细描述pthread_once
函数。
#include <string.h>
#include <errno.h>
#include <pthread.h>
#include <stdlib.h>
extern char **environ;
pthread_mutex_t env_mutex;
static pthread_once_t init_done = 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);
}
int
getenv_r(const char *name, char *buf, int buflen)
{
int i, len, olen;
pthread_once(&init_done, thread_init);
len = strlen(name);
pthread_mutex_lock(&env_mutex);
for (i = 0; environ[i] != NULL; i++) {
if ((strncmp(name, environ[i], len) == 0) && (environ[i][len] == '=')) {
olen = strlen(&environ[i][len+1]);
if (olen >= buflen) {
pthread_mutex_unlock(&env_mutex);
return(ENOSPC);
}
strcpy(buf, &environ[i][len+1]);
pthread_mutex_unlock(&env_mutex);
return(0);
}
}
pthread_mutex_unlock(&env_mutex);
return(ENOENT);
}
图12-12 getenv
的可重入(线程安全)版本
要使getenv_r
可重入,需要改变接口,调用者必须提供它自己的缓冲区,这样每个线程可以使用各自不同的缓冲区避免其他线程的干扰。但是,注意,要想使getenv_r
成为线程安全的,这样做还不够,需要在搜索请求的字符时保护环境不被修改。可以使用互斥量,通过getenv_r
和putenv
函数对环境列表的访问进行串行化。
可以使用读写锁,从而允许对getenv_r
进行多次并发访问,但增加的并发性可能并不会在很大程度上改善程序的性能,这里面有两个原因:
第一,环境列表通常并不会很长,所以扫描列表时并不需要长时间地占有互斥量;
第二,对getenv
和putenv
的调用也不是频繁发生的,所以改善它们的性能并不会对程序的整体性能产生很大的影响。
即使可以把getenv_r
变成线程安全的,这也不意味着它对信号处理程序是可重入的。如果使用的是非递归的互斥量,线程从信号处理程序中调用getenv_r
就有可能出现死锁。如果信号处理程序在线程执行getenv_r
时中断了该线程,这时我们已经占有加锁的env_mutex
,这样其他线程试图对这个互斥量的加锁就会被阻塞,最终导致线程进入死锁状态。所以,必须使用递归互斥量阻止其他线程改变我们正需要的数据结构,还要阻止来自信号处理程序的死锁。问题是pthread
函数并不保证是异步信号安全的,所以不能把pthread
函数用于其他函数,让该函数成为异步信号安全的。