UNIX环境高级编程 学习笔记 第十二章 线程控制
可用sysconf函数查询的线程限制:
但有些系统没提供访问这些限制的方法,但这些限制仍存在。
我们可以设置线程和线程属性或互斥量和互斥量属性来细调线程和同步对象的行为,管理这些属性的函数都遵循相同的模式:
1.每个对象与它自己类型的属性进行关联(即线程与线程属性关联,互斥量与互斥量属性关联)。一个属性对象可包含多个属性。应用程序不需了解属性对象内部结构的细节,这样可以增强程序可移植性(需要函数管理这些属性对象)。
2.有一个初始化函数将属性设为默认值。
3.由销毁属性对象的函数,如果初始化函数分配了与属性对象关联的资源,销毁函数负责释放这些资源。
4.每个属性都有一个从属性对象中获取属性值的函数。
5.每个属性都有一个设置属性值的函数,属性值按值传递。
pthread_create函数中,可传入指向pthread_attr_t结构的指针,用来修改线程默认属性,并把这些属性与创建的线程关联起来。使用以下函数初始化和反初始化pthread_attr_t结构:
调用pthread_attr_init后,pthread_attr_t就变成操作系统实现支持的线程属性的默认值。
pthread_attr_destroy函数反初始化pthread_attr_t,如果pthread_attr_init的实现对该属性对象的内存空间是动态分配的,则反初始化时会释放该空间,并将属性对象值设为无效的值,此时,如果该属性对象被误用,会导致pthread_create函数返回错误码。
如果对某个线程的终止状态不感兴趣,可用pthread_detach函数让操作系统在线程退出时收回它所占用的资源。
如果在线程创建时就知道不需要了解线程的终止状态,可修改pthread_attr_t结构中的detachstate线程属性,让线程开始时就处于分离状态。
可用pthread_attr_setdetachstate函数将detachstate设成以下值之一:PTHREAD_CREATE_DETACHED(以分离状态启动线程)、PTHREAD_CREATE_JOINABLE(正常启动线程):
以分离状态创建线程:
#include <pthread.h>
int makethread(void *(*fn)(void *), void *arg) { // 第一个参数是线程入口函数指针,返回类型为void *;第二个参数是要传给线程入口函数的参数
int err;
pthread_t tid;
pthread_attr_t attr;
err = pthread_attr_init(&attr);
if (err != 0) {
return err;
}
err = pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
if (err == 0) {
err = pthread_create(&tid, &attr, fn, arg);
}
pthread_attr_destroy(&attr);
return err;
}
上例忽略了pthread_attr_destroy函数的返回值,我们对线程属性进行了合理的初始化,应该不会失败,但有可能失败,最坏的情况就是初始化线程属性时动态分配的空间会丢失,造成内存泄漏,此时已经没有补救措施了,对线程属性进行清理的唯一接口就是pthread_attr_destroy函数,但它失败了。
POSIX的操作系统不一定支持线程栈属性,但SUS的XSI的操作系统一定支持线程栈属性。可在编译阶段使用_POSIX_THREAD_ATTR_STACKADDR或_POSIX_THREAD_ATTR_STACKSIZE符号检查系统是否支持线程栈属性,如定义了其中一个,就支持。也可在运行阶段把_SC_THREAD_ATTR_STACKADDR或_SC_THREAD_ATTR_STACKSIZE传给sysconf函数检查支持情况。
使用以下函数对线程栈属性进行管理:
对进程来说,虚地址空间大小固定。但对线程来说,同样大小的虚地址空间被所有线程栈共享,如果有很多线程,以致这些线程栈的大小加起来超出了可用的虚地址空间,就需要减少默认的线程栈大小;还可能线程创建了大量的自动变量,或调用的函数涉及许多很深的栈帧,那么帧大小可能比默认帧大小更大。
如果线程栈的虚地址空间都用完了,可用malloc或mmap函数分配空间替代原空间,并调用pthread_attr_setstack来改变新建的栈的位置。由stackaddr参数指定的地址用作线程栈的内存范围中的最低可寻址地址,该地址与处理器结构相应的边界应对齐。
pthread_attr_t结构中的线程属性成员stackaddr被定义为栈的最低内存地址,这并不一定是栈的开始位置,如果栈是从高地址向低地址方向增长的(如x86模型),则stackaddr线程属性是栈的结尾位置而非开始位置。
使用以下函数只改变或获取线程属性stacksize:
可用以上函数改变默认的栈大小,且不用自己处理线程栈的分配问题。设置stacksize属性时,其值不能小于PTHREAD_STACK_MIN的值。
线程属性guardsize控制线程栈末尾之后的用以避免栈溢出的扩展内存大小。此属性默认值取决于具体实现,常用值是系统页大小。可把guardsize设为0,此时不会提供警戒缓冲区。如果修改了线程属性stackaddr,系统就认为我们将自己管理栈,这使得警戒缓冲区机制无效,等同于把guardsize设为0。
如果guardsize线程属性被修改,操作系统可能会将其取为页大小的整数倍。当线程的栈指针溢出到警戒区域,应用可能通过信号接收到出错信息。
还有其他的一些线程属性。
互斥量属性用结构pthread_mutexattr_t结构表示。可用以下函数初始化和反初始化互斥量:
pthread_mutexattr_init函数用默认的互斥量属性初始化pthread_mutexattr_t结构。它有一种属性是进程共享属性,在POSIX.1中是可选的,可通过检查是否定义了_POSIX_THREAD_PROCESS_SHARED判断平台是否支持进程共享属性,也可以运行时把_SC_THREAD_PROCESS_SHARED参数传给sysconf进行检查。遵循XSI的系统一定支持此选项。
默认,多个线程可以访问同一个进程中的同一个同步对象,此时,进程共享互斥量属性设为PTHREAD_PROCESS_PRIVATE。还有一种机制是允许相互独立的多个进程把同一个内存数据块映射到它们各自独立的地址空间中,就像多个线程访问共享数据一样, 多个进程访问共享数据也需同步,此时,可将互斥量属性进程共享设为PTHREAD_PROCESS_SHARED,于是从多个进程共享的内存数据块中分配的互斥量就可用于这些进程的同步。
使用以下函数查询和修改互斥量的进程共享属性:
互斥量的进程共享属性设为PTHREAD_PROCESS_PRIVATE时,pthread线程库会提供更有效率的互斥量实现。
使用以下函数获取互斥量的健壮属性:
在多进程间共享互斥量时,当锁定互斥量的进程终止时,其他阻塞在这个锁的进程将会一直阻塞下去。
健壮属性有两种可能的取值,默认值是PTHREAD_MUTEX_STALLED,此时持有互斥量的进程终止时不采取特别的动作,等待该互斥量解锁的进程会一直等待。另一个取值为PTHREAD_MUTEX_ROBUST,此时会导致阻塞的线程从pthread_mutex_lock函数返回并获取锁,但返回值为EOWNERDEAD而非0,这个返回值不代表错误,因为调用者已经拥有了锁。这个返回值指示应用应恢复互斥量保护的状态。
使用健壮的互斥量时,pthread_mutex_lock函数的返回值为三种:
1.不需要恢复的成功。
2.需要恢复的成功。
3.失败。
如果应用的互斥量保护的状态无法恢复,线程对互斥量解锁后,该互斥量将处于永久不可用状态,为避免这种情况,必须在解锁前调用以下函数指明该互斥量相关的状态是一致的:
如果线程没有先调用pthread_mutex_consistent就解锁了互斥量,那么其他获取该互斥量时阻塞的线程会得到错误码ENOTRECOVERALBLE,如果发生这种情况,互斥量就不再可用。
互斥量的类型属性控制着互斥量的锁定特点,POSIX.1定义了4种类型:
1.PTHREAD_MUTEX_NORMAL。标准互斥量类型,不做特殊的错误检查和死锁检测。
2.PTHREAD_MUTEX_ERRORCHECK。此互斥量类型提供错误检查,检查的情况如下表。
3.PTHREAD_MUTEX_RECURSIVE。此互斥量类型允许同一线程在互斥量解锁前对该互斥量多次加锁。递归互斥量维护锁的计数,只有解锁次数等于加锁次数时,才解锁成功。
4.PTHREAD_MUTEX_DEFAULT。提供默认特性和行为,操作系统实现它时,可将它映射为以上三种中的任一种类型。
不占用时解锁指解锁另一个线程加锁的互斥量。
已解锁时解锁指解锁未锁定的互斥量。
使用以下函数获取和设置互斥量的类型属性:
假设func1和func2是函数库中现有函数,其接口不能改变,于是我们将互斥量嵌入到了数据结构中,并把这个结构的地址作为参数传入。如果func1和func2都要操作这个结构,可能会有一个以上的线程同时访问该数据结构,那么func1和func2必须在操作数据前对互斥量加锁。
如果func1必须调用func2(如上图),这时如果互斥量不是递归类型的,就会出现死锁。如果能在func1调用func2前释放互斥量,在func2返回后再重新获取互斥量,就可以避免使用递归互斥量,但这给了其他线程竞争互斥量的机会,使得其他线程也修改了这个结构,这可能会造成数据失效,因为在func1还没修改完此结构时,它就被其他线程修改了。
如上图,通过提供func2函数私有的、不用加锁的版本,可保持func1和func2函数接口不变,且避免了使用递归互斥量。
如果可以改变函数接口,可在func2函数的参数中增加一个参数以说明这个结构是否被调用者锁定,如锁定,则不用再加锁。
使用递归互斥量的另一种情况,有一个超时函数,它安排另一个函数在某一时间运行,可为每个挂起的超时函数创建一个线程,在时间未到时一直等待:
#include <pthread.h>
#include <time.h>
#include <sys/time.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <iostream>
using namespace std;
int makethread(void *(*fn)(void *), void *arg) { // 第一个参数是线程入口函数指针,返回类型为void *;第二个参数是要传给线程入口函数的参数
int err;
pthread_t tid;
pthread_attr_t attr;
err = pthread_attr_init(&attr);
if (err != 0) {
cout << "init pthread_attr_t error" << endl;
return err;
}
err = pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
if (err == 0) {
err = pthread_create(&tid, &attr, fn, arg);
cout << "create pthread success" << endl;
}
pthread_attr_destroy(&attr);
return err;
}
struct to_info {
void (*to_fn)(void *); // function
void *to_arg; // argument
struct timespec to_wait; // time to wait
};
#define SECTONSEC 1000000000 // seconds to nanoseconds
#if !defined(CLOCK_REALTIME) || defined(BSD)
#define clock_nanosleep(ID, FL, REQ, REM) nanosleep((REQ), (REM))
#endif
#ifndef CLOCK_REALTIME
#define CLOCK_REALTIME 0
#define USECTONSEC 1000 // microseconds to nanoseconds
void clock_gettime(int id, struct timespec *tsp) {
struct timeval tv;
gettimeofday(&tv, NULL);
tsp->tv_sec = tv.tv_sec;
tsp->tv_nsec = tv.tv_usec * USECTONSEC;
}
#endif
void *timeout_helper(void *arg) {
struct to_info *tip;
printf("in timeout_helper");
tip = (struct to_info *)arg;
clock_nanosleep(CLOCK_REALTIME, 0, &tip->to_wait, NULL);
printf("after nanosleep");
(*tip->to_fn)(tip->to_arg);
free(arg);
return 0;
}
void timeout(const struct timespec *when, void (*func)(void *), void *arg) {
struct timespec now;
struct to_info *tip;
int err;
clock_gettime(CLOCK_REALTIME, &now);
cout << "when sec: " << when->tv_sec << " nsec: " << when->tv_nsec << endl;
cout << "now sec: " << now.tv_sec << " nsec: " << now.tv_nsec << endl;
if ((when->tv_sec > now.tv_sec) || (when->tv_sec == now.tv_sec && when->tv_nsec > now.tv_nsec)) {
tip = (to_info *)malloc(sizeof(struct to_info));
if (tip != NULL) {
printf("going to set to_info\n");
tip->to_fn = func;
tip->to_arg = arg;
tip->to_wait.tv_sec = when->tv_sec - now.tv_sec;
if (when->tv_nsec >= now.tv_nsec) {
tip->to_wait.tv_nsec = when->tv_nsec - now.tv_nsec;
} else {
tip->to_wait.tv_sec--;
tip->to_wait.tv_nsec = SECTONSEC - now.tv_nsec + when->tv_nsec;
}
err = makethread(timeout_helper, (void *)tip);
if (err == 0) {
return;
cout << "make thread success" << endl;
} else {
free(tip);
cout << "make thread error, err = " << err << endl;
}
}
}
printf("when <= now");
// we get there if (a) when <= now, or (b) malloc fails, or (c) we can't make a thread
// so we just call the function now
(*func)(arg);
}
pthread_mutexattr_t attr;
pthread_mutex_t mutex;
void retry(void *arg) {
pthread_mutex_lock(&mutex);
// perform retry steps
printf("in retry\n");
pthread_mutex_unlock(&mutex);
}
int main() {
int err, condition = 1, arg;
struct timespec when;
if ((err = pthread_mutexattr_init(&attr)) != 0) {
printf("%d, pthread_mutexattr_init failed!", err);
exit(1);
}
if ((err = pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE)) != 0) {
printf("%d, can't set recursive type", err);
exit(1);
}
if ((err = pthread_mutex_init(&mutex, &attr)) != 0) {
printf("%d, can't create recursive mutex", err);
exit(1);
}
// continue processing
pthread_mutex_lock(&mutex); // 此处上锁使检查condition和执行timeout成为一个原子操作
if (condition) {
clock_gettime(CLOCK_REALTIME, &when);
when.tv_sec += 10; // 10 seconds from now
timeout(&when, retry, (void *)((unsigned long)arg));
}
pthread_mutex_unlock(&mutex);
sleep(15); // 保证新建的线程能等到睡眠结束并执行完程序,或主线程还可进行其他操作
exit(0);
}
检查condition的锁和要执行的函数中的锁是一个,因此当timeout中出问题时,如不能创建线程、安排函数运行的时间已过、malloc调用失败时,timeout可以直接在最后调用函数,此时由于使用了递归互斥量,避免死锁或要返回main函数解锁后再调用该函数。正常情况应该是main函数调用timeout,timeout函数调用makethread创建线程,之后创建的线程等待时间到来,而函数makethread返回函数timeout,然后timeout函数返回main函数,之后再在main中解锁互斥量,然后时间到达后,进入要执行的函数,在其中再加锁运行。
我们创建的是分离的线程,这是由于retry函数将在未来运行,我们不希望调用pthread_join阻塞自己空等线程。
使用以下函数初始化和反初始化读写锁属性结构:
读写锁只支持进程共享属性,它与互斥量的进程共享属性是相同的,使用以下函数用于读取和设置读写锁的进程共享属性:
初始化和反初始化条件变量:
SUS定义了条件变量的两个属性:进程共享属性和时钟属性,获取和改变它们:
条件变量的时钟属性控制计算pthread_cond_timewait函数的超时参数时采用的是哪个时钟。
SUS并没有为其他有超时等待函数的同步对象的属性对象定义时钟属性。
使用以下函数初始化和反初始化屏障属性:
屏障属性只有进程共享属性:
进程共享属性的值可以是PTHREAD_PROCESS_SHARED或PTHREAD_PROCESS_PRIVATE。
线程类似于信号处理程序,可能同时访问一个函数两次。
如果一个函数在一个时间点可以被多个线程使用,就称该函数是线程安全的。
除了以上函数不是线程安全的之外,传入空指针的ctermid和tmpnam函数也不是线程安全的。
如果wcrtomb和wcsrtombs的参数mbstate_t传入的是空指针,也不能保证是线程安全的。
支持线程安全函数的系统会在unistd.h中定义符号_POSIX_THREAD_SAFE_FUNCTIONS。也可以给sysconf函数传入_SC_THREAD_SAFE_FUNCTIONS参数在运行时检查是否支持线程安全的函数。
SUSv4之前,所有遵循XSI的实现都必须支持线程安全函数,但在SUSv4中,线程安全函数的支持必须考虑遵循POSIX。
对于POSIX.1中的一些非线程安全函数,会提供可替代的线程安全版本:
这些函数比它们的非线程安全版本的名字多了_r结尾,表明这些版本是可重入的。很多函数不是线程安全的原因在于它们返回的数据存放在静态的内存缓冲区。通过修改接口,要求调用者自己提供缓冲区可以使函数变为线程安全的。
如果一个函数对多个线程来说是可重入的,那么这个函数就是线程安全的。但这并不能说明对信号处理程序来说,该函数也是可重入的。如果函数对异步信号程序的重入是安全的,那么可以说函数是异步信号安全的。
除了上图可替代的线程安全函数外,POSIX.1还提供了以线程安全方式管理FILE对象的方法,可用flockfile和ftrylockfile函数获取给定FILE对象关联的锁,这个锁是递归的。要求所有操作FILE对象的标准IO例程的动作看起来就像在它们内部调用了flockfile和funlockfile。
以上函数允许应用把多个对标准IO函数的调用组合成原子序列。
递归锁只是首次加锁的线程可以多次加锁,加锁线程未解锁时其他线程不能加锁。
如果标准IO例程都获取它们各自的锁,那么在做一次一个字符的IO时,性能会下降很多,此时,需要对每个字符的读写操作进行获取锁和释放锁的动作,以下是不加锁版本的基于字符的标准IO例程:
除非被flockfile或ftrylockfile的调用包围,否则尽量不要用这4个函数,这可能会引起其他控制线程非同步地访问数据导致的问题。
一旦对FILE对象加锁,就能在释放锁之前对这些函数进行多次调用,这样可以在多次的数据读写上分摊总的加解锁开销。
getenv函数的非可重入版本,当两个线程同时调用它时,会看到不一致的结果,因为所有调用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;
}
以下是getenv函数的可重入版本,它使用pthread_once函数来确保不管多少线程同时竞争getenv_r函数,每个进程只调用thread_init一次:
#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() {
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; // 没有这样的文件或目录错误
}
调用者提供自己的缓冲区,这样每个线程可以使用各自不同的缓冲区以避免其他线程的干扰。要想使getenv_r函数成为线程安全的,还需在搜索请求的字符时避免其他线程的干扰,以上我们可以使用一个互斥量,通过getenv_r函数来访问环境列表(putenv函数使用前后也会操作此互斥量),来使得访问是线程安全的。
可以使用读写锁,从而允许对getenv_r函数进行并发访问,但可能并不会很大程度上改善性能,这是由于:第一,环境列表通常不会很长,扫描列表时不需要长时间占有互斥量;第二,对getenv和putenv的调用也不是频繁发生的,因此改善对环境列表的访问不会对程序整体性能产生很大影响。
即使getenv_r函数是线程安全的,这也不意味着它对信号处理程序是可重入的,如果使用非递归互斥量,线程从信号处理程序中调用getenv_r就可能出现死锁。如果信号处理程序在线程调用getenv_r时中断了该线程,此时我们已占有加锁的env_mutex,这样信号处理程序试图对这个互斥量加锁时会被阻塞,最终导致线程进入死锁状态。因此,我们必须使用递归互斥量。
pthread函数不能保证是异步信号安全的,所以不能把pthread函数用于其他想设计成异步信号安全的函数中。
线程特定数据也被称为线程私有数据,是存储和查询某个特定线程相关数据的一种机制。我们使用它可以使每个线程访问它自己单独的数据副本,而不需担心与其他线程的同步访问问题。
使用线程特定数据的原因有两个:
1.有时候我们需要维护基于每个线程的数据,而线程ID不能保证是小而连续的整数,因此就不能简单地使用线程ID作为每个线程数据组成的数组的下标。即使线程ID是小而连续的整数,我们还需额外的保护,使得某个线程的数据不会与其他线程的数据相混淆。
2.它让基于进程的接口适应多线程的环境机制。比如errno,线程出现之前的接口把errno定义为进程上下文中全局可访问的整数,在系统调用或库例程调用或执行失败时设置errno,把它作为操作失败的附属结果。而现在errno被定义为线程私有数据,这样一个线程设置了errno也不会影响到其他线程中的errno值。
一个进程中的所有线程都能访问这个进程整个地址空间,除了使用寄存器,一个线程无法阻止另一个线程访问它的数据,线程特定数据也不例外,虽然底层实现不能阻止这种访问能力,但管理线程特定数据的函数也能提高线程间的独立性,使线程不太容易访问到其他线程的线程特定数据。
分配线程特定数据前,要先创建与该数据关联的键,键将用于获取对线程特定数据的访问,创建一个键:
创建的键存储在参数keyp指向的内存单元,这个键可被进程中所有线程使用,但每个线程把这个键与不同的特定数据地址进行关联。创建新键时,每个线程的数据地址都被设为空值。
参数destructor是一个可选的、与键关联的析构函数,当一个线程退出时,如果数据地址是非空值,那么析构函数就会调用,该析构函数的参数就是数据地址,如果析构函数为空,则没有析构函数与该键关联。当线程调用pthread_exit或线程执行返回、正常退出时,析构函数就会被调用;线程取消时,只有在最后的清理处理程序返回后,析构函数才被调用;线程调用exit、_exit、_Exit或abort,或出现其他非正常退出情况时,不会调用析构函数。
线程通常用malloc函数为线程特定数据分配内存,析构函数通常释放已分配的内存。如果线程没有释放内存就退出了,那么这块内存就会丢失,线程所属进程就出现了内存泄漏。
每个操作系统的键数量有限制(PTHREAD_KEYS_MAX)。
线程退出时,线程特定数据的析构函数按OS的实现定义的顺序被调用,析构函数可能会调用另一个函数,该函数可能会创建新的线程特定数据,并且把这个数据与当前键关联起来,当所有析构函数调用完成后,系统会检查是否还有非空的线程特定数据与键关联,如果有,再次调用析构函数,这个过程会一直重复直到线程所有键都是空的线程特定值或已经做了PTHREAD_DESTRUCTOR_ITERATIONS定义的最大次数的尝试。
取消键和线程特定数据之间的关联:
但该函数不会调用与键关联的析构函数。
分配的键可能由于初始化阶段的竞争而发生变动:
void destructor(void *);
pthread_key_t key;
int init_done = 0;
int threadfunc(void *arg) {
if (!init_done) {
init_done = 1;
err = pthread_key_create(&key, destructor);
}
}
有些线程可能看到一个键值,而其他线程可能看到其他键值,这取决于系统是如何调度线程的。解决这种竞争的方法是调用pthread_once:
参数intflag必须是一个非本地变量(如全局变量或静态变量),且必须初始化为PTHREAD_ONCE_INIT。
如果每个线程都调用pthread_once,那么只有首次调用它的线程能成功。
创建键时避免出现冲突的方法:
void destructor(void *);
pthread_key_t key;
pthread_once_t init_done = PTHREAD_ONCE_INIT;
void thread_init() {
err = pthread_key_create(&key, destructor);
}
int threadfunc(void *arg) {
pthread_once(&init_done, thread_init);
}
创建好键后,就可以把键和线程特定数据关联起来:
如果没有线程特定数据与键关联,pthread_getspecific函数返回空指针,可通过此值确定是否需要调用pthread_setspecific。
用线程特定数据维护每个线程的数据缓冲区副本,存放各自返回字符串的geten函数:
#include <limits.h>
#include <string.h>
#include <pthread.h>
#include <stdlib.h>
#define MAXSTRINGSZ 4096
static pthread_key_t key;
static pthread_once_t init_done = PTHREAD_ONCE_INIT;
pthread_mutex_t env_mutex = PTHREAD_MUTEX_INITIALIZER;
extern char **environ;
static void thread_init() {
pthread_key_create(&key, free);
}
char *getenv(const char *name) {
int i, len;
char *envbuf;
pthread_once(&init_done, thread_init); // 确保第一个调用的线程创建键
pthread_mutex_lock(&env_mutex);
envbuf = (char *)pthread_getspecific(key);
if (envbuf == NULL) { // 如果是空指针,需要先关联键和线程特定数据
envbuf = malloc(MAXSTRINGSZ);
if (envbuf == NULL) {
pthread_mutex_unlock(&env_mutex);
return NULL;
}
pthread_setspecific(key, envbuf); // 关联键和线程特定数据
}
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);
pthread_mutex_unlock(&env_mutex);
return envbuf;
}
}
pthread_mutex_unlock(&env_mutex);
return NULL;
}
这个版本的getenv函数是线程安全的,但不是异步信号安全的,对信号处理程序而言,即使使用了递归互斥量,但其中调用了malloc,malloc函数本身不是异步信号安全的。
线程属性可取消状态和可取消类型没有包含在pthread_attr_t结构中,这两个属性影响着线程在响应pthread_cancel函数时呈现的行为。
可取消状态属性可取两个值:PTHREAD_CANCEL_ENABLE和PTHREAD_CANCEL_DISABLE。可通过以下函数修改:
pthread_setcancelstate函数可把当前可取消状态设为state,同时也可把原来的可取消状态存放在参数oldstate指向的内存单元中,这两步是一个原子操作。
pthread_canel调用并不等待要取消的线程终止,默认,要被取消的线程在取消请求发出后会继续运行,直到要被取消的线程到达某个取消点,取消点是线程检查它是否被取消的位置,如果取消了,则按请求行事。POSIX.1保证线程在调用以下函数时取消点会出现:
线程启动时默认的可取消状态是PTHREAD_CANCEL_ENABLE,当状态设为PTHREAD_CANCEL_DISABLE时,调用pthread_cancel不会杀死线程,而是挂起取消请求,当取消状态再次变为PTHREAD_CANCEL_ENABLE时,线程将在下一个取消点上处理挂起的取消请求。
如果线程长时间不会调用一些有取消点的函数,可用以下函数添加取消点:
调用以上函数时,如果有某个取消请求正处于挂起状态,且线程状态属性为可被取消(PTHREAD_CANCEL_ENABLE),则线程就会被取消,如果线程取消属性为不可被取消(PTHREAD_CANCEL_DISABLE),则该函数调用没有任何效果。
可通过以下函数设置取消类型:
取消类型可以是PTHREADCANCEL_DEFERRED(默认,也称推迟取消,在线程到达取消点前,不会真正取消)和PTHREAD_CANCEL_ASYNCHRONOUS(不用遇到取消点就能被取消)。
pthread_setcanceltype函数会把原来的取消类型存放到oldtype参数指针指向的整型单元中。
每个线程都有自己的信号屏蔽字,但信号的处理是进程中所有线程共享的,这意味着当某个线程修改了与某个给定信号相关的处理行为后,所有线程都共享这个处理行为的改变。
进程中的信号是递送给单个线程的,如果一个信号与硬件故障相关,那么该信号一般会发送到引起该事件的线程中,其他信号会被发送到任意一个线程。
进程使用sigprocmask函数阻止信号发送,但该函数行为在多线程的进程中没有定义,线程必须使用pthread_sigmask函数:
pthread_sigmask与sigprocmask函数基本相同,但它工作在线程中,且失败时返回错误码(不像sigprocmask返回-1并设置errno)。set参数包含线程要操作的信号集;how参数是以下3个值之一:
1.SIG_BLOCK:把参数set表示的信号集中所有信号添加到线程信号屏蔽字中。
2.SIG_SETMASK:把线程屏蔽字设为参数set表示的信号集中的所有信号。
3.SIG_UNBLOCK:从线程信号屏蔽字中删除set表示的信号集中的信号。
如果oset参数不为空,把之前的信号屏蔽字存储在该参数指向的sigset_t结构中。线程可将set参数设为NULL(此时how参数会被忽略),并传入oset参数来获取线程当前的屏蔽字。
线程调用sigwait可等待一个或多个信号的出现:
set参数指定了线程要等待的信号集,返回时将接收到的信号编号存放到signop参数指向的整型单元。
如果信号集中的某信号在调用sigwait时处于挂起状态,则sigwait函数将无阻塞地返回,返回前,sigwait函数从进程挂起的信号集中移除此信号;如果实现支持排队信号,且该信号的多个实例被挂起,那么sigwait函数只会移除该信号的一个实例,其他实例还要继续排队。
为了避免错误,线程在调用sigwait前,必须先阻塞它要等待的信号。sigwait函数会原子地取消在等待信号的阻塞和一个信号被递送。在返回前,sigwait函数会恢复线程的信号屏蔽字。如果信号在sigwait调用时没有被阻塞,那么在线程完成对sigwait的调用前会出现一个时间窗,此时信号就可以被发送给线程,从而进入该信号的信号处理程序或按系统默认方式处理该信号。
sigwait函数可简化信号的处理,允许把异步产生的信号用同步的方式处理,为防止信号中断线程,可将信号加到每个线程的信号屏蔽字中,然后安排专用的线程处理信号,处理信号的方式是进行正常的函数调用,而非传统方式那样可能会中断某些函数的调用。
如果多个线程在sigwait调用中因等待同一个信号而阻塞,那么在信号递送时,只有一个线程可以从sigwait函数返回。如果一个信号有它自己的信号处理程序,同时一个线程在sigwait此信号,那么信号如何递送由操作系统实现决定,操作系统实现可以让sigwait函数返回,也可以激活信号处理程序,但这两种情况不会同时发生。
kill函数把信号发给进程,pthread_kill把信号发给线程:
当signo参数为0时,相当于检查线程是否存在。如果信号的默认处理动作是终止进程,那么该线程所在的整个进程会被杀死。
闹钟定时器是所有线程共享的。
当一个进程中的部分线程阻塞了某信号,此时信号发生,会递送到未阻塞此信号的线程中:
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <pthread.h>
#include <signal.h>
void *threadFunc(void *param) {
sigset_t maskSigSet;
if (sigemptyset(&maskSigSet) == -1) {
printf("sigemptyset error\n");
exit(1);
}
if (sigaddset(&maskSigSet, SIGALRM) == -1) {
printf("sigaddset error\n");
exit(1);
}
int err;
if ((err = pthread_sigmask(SIG_SETMASK, &maskSigSet, NULL)) != 0) {
printf("pthread_sigmask error\n");
exit(1);
}
int sigRec = 0;
if ((err = sigwait(&maskSigSet, &sigRec)) != 0) {
printf("sigwait error\n");
exit(1);
}
switch (sigRec) {
case SIGALRM:
printf("in threadFunc: SIGALRM received\n");
break;
}
while (1) ;
}
int main() {
int err = 0;
pthread_t pid;
if ((err = pthread_create(&pid, NULL, &threadFunc, NULL)) != 0) {
printf("pthread_create error\n");
exit(1);
}
alarm(2);
while(1) ;
exit(0);
}
由于SIGALRM默认行为是终止进程,因此当SIGALRM被递送到阻塞并等待此信号的线程时,进程不会结束,而当此信号被递送到未阻塞此信号的线程时,进程会结束,多次运行以上程序:
可见每次信号都被递送到未阻塞此信号的线程。如果所有线程都阻塞了此信号:
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <pthread.h>
#include <signal.h>
void *threadFunc(void *param) {
sigset_t maskSigSet;
if (sigemptyset(&maskSigSet) == -1) {
printf("sigemptyset error\n");
exit(1);
}
if (sigaddset(&maskSigSet, SIGALRM) == -1) {
printf("sigaddset error\n");
exit(1);
}
int err;
if ((err = pthread_sigmask(SIG_SETMASK, &maskSigSet, NULL)) != 0) {
printf("pthread_sigmask error\n");
exit(1);
}
int sigRec = 0;
if ((err = sigwait(&maskSigSet, &sigRec)) != 0) {
printf("sigwait error\n");
exit(1);
}
switch (sigRec) {
case SIGALRM:
printf("in threadFunc: SIGALRM received\n");
break;
}
while (1) ;
}
int main() {
int err = 0;
pthread_t pid;
if ((err = pthread_create(&pid, NULL, &threadFunc, NULL)) != 0) {
printf("pthread_create error\n");
exit(1);
}
alarm(2);
sigset_t maskSigSet;
if (sigemptyset(&maskSigSet) == -1) {
printf("sigemptyset error\n");
exit(1);
}
if (sigaddset(&maskSigSet, SIGALRM) == -1) {
printf("sigaddset error\n");
exit(1);
}
if ((err = pthread_sigmask(SIG_SETMASK, &maskSigSet, NULL)) != 0) {
printf("pthread_sigmask error\n");
exit(1);
}
int sigRec = 0;
if ((err = sigwait(&maskSigSet, &sigRec)) != 0) {
printf("sigwait error\n");
exit(1);
}
switch (sigRec) {
case SIGALRM:
printf("in main: SIGALRM received\n");
break;
}
while(1) ;
exit(0);
}
多次运行以上程序:
当所有线程都阻塞此信号并等待此信号发生时,信号只会被递送到一个线程。
多线程下忽视SIGINT,同时正常处理SIGQUIT:
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <pthread.h>
int quitflag; // set nonzero by thread
sigset_t mask;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t waitloc = PTHREAD_COND_INITIALIZER;
void *thr_fn(void *arg) {
int err, signo;
for (; ; ) {
err = sigwait(&mask, &signo);
if (err != 0) {
printf("sigwait error\n");
exit(1);
}
switch (signo) {
case SIGINT:
printf("interrupt\n");
break;
case SIGQUIT:
pthread_mutex_lock(&lock);
quitflag = 1;
pthread_mutex_unlock(&lock);
pthread_cond_signal(&waitloc);
return 0;
default:
printf("unexpected signal %d\n", signo);
exit(1);
}
}
}
int main () {
int err;
sigset_t oldmask;
pthread_t tid;
sigemptyset(&mask);
sigaddset(&mask, SIGINT);
sigaddset(&mask, SIGQUIT);
if ((err = pthread_sigmask(SIG_BLOCK, &mask, &oldmask)) != 0) {
printf("pthread_sigmask error\n");
exit(1);
}
err = pthread_create(&tid, NULL, thr_fn, 0);
if (err != 0) {
printf("can't create thread\n");
exit(1);
}
pthread_mutex_lock(&lock);
while (quitflag == 0) { // 防止接收信号的线程已经接收到信号且调用了pthread_cond_signal,main中还未首次到达此处
pthread_cond_wait(&waitloc, &lock);
}
pthread_mutex_unlock(&lock);
// SIGQUIT has been caught and is now blocked; do whatever
quitflag = 0;
// reset signal mask which unblocks SIGQUIT
if (sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0) {
printf("SIG_SETMASK error\n");
exit(1);
}
exit(0);
}
运行它:
新线程会继承现有信号屏蔽字。所有线程中只有一个线程用于信号接收,在对主线程编码时不必担心来自这些信号的中断。
线程调用fork时,会为子进程创建整个进程地址空间的副本,即会从父进程处继承每个互斥量、读写锁、条件变量的状态,如果父进程包含一个以上的线程,子进程在fork调用返回后,如果紧接着不是马上调用exec,就需要清理锁状态。在子进程内部,只有一个线程,它是由父进程中调用fork的线程的副本构成的,如果父进程中其他线程占有锁,子进程将同样占有这些锁,但子进程中不包含占有锁的线程的副本,所以子进程不知道它占有了哪些锁、需要释放哪些锁。
POSIX.1声明,在fork调用返回和子进程调用其中一个exec函数之间,子进程只能调用异步信号安全的函数,这限制了在exec前子进程能做的事,但还是没有处理子进程中锁状态的问题。
可通过调用pthread_atfork建立fork处理程序,来清除锁状态:
prepare参数指向的处理程序由父进程调用的fork创建子进程前调用,作用是获取父进程定义的所有锁;parent参数指向的处理程序在调用fork创建了子进程后,返回前在父进程的上下文中调用,作用是解锁prepare参数指向的处理程序获取的所有锁;child参数指向的处理程序在fork调用返回前的子进程上下文中调用,作用是释放prepare参数指向的处理程序获取的所有锁。
可多次调用pthread_atfork从而设置多套fork处理程序,如果不需要使用其中某个处理程序,可传入空指针。使用多套fork处理程序时,parent和child处理程序是按它们注册时的顺序调用的,而prepare处理程序的调用顺序与它们注册时的顺序相反,这样可以允许多个模块注册它们自己的fork处理程序,且保持锁的层次。
假如模块A调用模块B中的函数,且每个模块都有自己的一套锁,如果锁的层次是A在B前,模块B必须在模块A前设置它的fork处理程序,父进程调用fork时,假设子进程先运行,会执行以下步骤:
fork处理程序用来清除锁状态,对于条件变量的状态,在有些操作系统中,条件变量可能不需做任何清理;但有些操作系统将锁作为条件变量实现的一部分,此时条件变量就需要清理,此时调用fork后就不能使用条件变量。如果操作系统的实现是使用全局锁保护进程中所有的条件变量数据结构,那么操作系统实现本身可以在fork库例程中做锁的清理工作,但程序不应依赖操作系统实现中类似这样的细节。
使用pthread_atfork函数: