我们在10.6节中讨论了可重入函数以及信号处理函数,线程与信号处理函数在重入方面是非常相似的。多线程的控制的本质也是同时调用相同的函数。
如果一个函数可以被多个线程同时安全地调用,我们称该函数为线程安全的(thread-safe).除了图12.9列出的函数之外,The Single Unix Specification定义的其他函数都是线程安全的,此外,ctermid以及tmpnam函数在传入参数是null指针的时候不能保证是线程安全的,类似地,wcrtomb以及wcsrtombs的mbstate_t参数传入为null指针的时候可能是不安全的。
支持线程安全的实现会在头文件中定义标志_POSIX_THREAD_SAFE_FUNCTIONS,应用程序也可以使用函数sysconf传参_SC_THREAD_SAFE_FUNCTIONS在运行时进行检查。在the Single Unix Specification第四版之前,所有兼容XSI选项的实现都要求支持线程安全函数,在SUSv4中,只要兼容POSIX标准的实现都要求支持线程安全函数。
对于POSIX.1中不是线程安全的函数而言,实现也提供了对应的线程安全的版本,图12.10列出了这些函数的线程安全版本,这些函数的名称与它们的非线程安区版本基本相同,但是在名字最后增加了一个_r,表示这些函数是可以重入的,许多函数并不是线程安全的,因为他们的返回数据存储在静态内存缓冲区中,通过修改这些函数的接口,将它们的接口修改为调用者提供的缓冲区就可以实现线程安全了。
如果一个函数对于多线程而言是可重入的,那么我们称该函数是线程安全的,但是,这并不能说明该函数对于信号处理而言是可重入的。如果一个函数对于异步信号处理而言是可重入的,那么称该函数未异步信号安全的(async-signal safe).在10.6节中的图10.4已经总结除了所有异步信号处理安全的函数。
除了列在12.10中的函数之外,POSIX.1还提供了一种线程安全的方式来管理FILE对象,你可以使用flockfile以及ftrylockfile来获取与给定FILE对象关联的锁,该锁是一个递归锁,因此即是当前线程已经锁定了该锁,即使你再次获取该锁也不会出现死锁的情况,谁然锁的具体实现没有指定,但是所有操作FILE对象的标准IO函数都要求表现得像它们在内部调用了函数flockfile以及funlockfile一样。
#include <stdio.h>
int ftrylockfile(FILE *fp);
return: 0 if OK, nonzero if lock can;t be acquired.
void flockfile(FILE *fp);
void funlockfile(FILE *fp);
虽然标准IO函数已经在其内部实现了线程安全,但是提供锁的操作给应用程序还是非常有用的,这允许应用程序将多个标准IO操作组合成一个原子操作。当然,当处理多个FILE对象的时候,需要注意潜在的死锁风险,需要仔细对锁定进行排序。
如果标准IO程序在内部获取和释放锁,当我们执行一次一个字符的IO操作的时候,性能将会急剧恶化,在这种情况下,我们不能再为每一个字符的读写获取和释放锁,为了减少资源的浪费,提供了一些基于字符的无锁定操作的标准IO版本。
#include <stdio.h>
int getchar_unlocked(void);
int getc_unlocked(FILE *fp);
Both return:the next character if OK, EOF on end of file or error.
int putchar_unlocked(int c);
int putc_unlocked(int c, FILE *fp);
Both return: c if OK, EOF on error.
这四个函数不知简单地进行调用,除非使用函数flockfile(或者是ftrylockfile)以及funlockfile进行包围,否则,可能出现不可预知的后果。
一旦锁定了FILE对象,你就可以多次调用上述函数了,锁定与解锁的开销会被多次的数据读写进行分摊。
Example
图12.11 显示了一个getemv函数的可能的实现(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来保证thread_init函数只能被一个进程调用一次,而不管同时由多少个线程调用了函数getenv_r.关于函数pthread_once,在12.6节中将会进行更加深入的介绍。
#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成为线程安全的,我们还需要防止在我们对环境变量进行搜索的时候发生改变,我们可以使用同一个互斥锁来保证get_env_r与putenv函数能够串行访问。
我们也可以使用读写锁来允许多个线程并行访问get_env_r函数,但是并行性的增加并不会对性能由明显改善,原因有二:首先,环境变量列表一般来说并不长,所以在搜索列表的时候,我们并不需要锁定互斥锁很长时间;其次,对函数getenv以及putenv函数的调用次数通常都是很少的,因此,即使我们改善了这两个函数的性能,但是对于应用程序的整体性能并不会由多大影响。
即使我们可以使得函数getenv_r函数成为线程安全的,但是并不意味这该函数对于信号处理函数而言是安全的。如果我们使用了非递归互斥锁,在运行过程中就存在死锁的风险,当信号处理函数中断了一个正在执行getenv_r函数的线程,在线程中可能已经获取到了互斥锁,所以信号处理函数中如果再次尝试获取互斥锁将会造成线程死锁,因此,我们必须使用一个递归锁以防止信号处理函数中发生死锁。问题是,由于pthread函数并不保证是异步信号安全的,因此我们也不能使用它们来编写另一个异步信号安全的函数。