apue学习笔记(第十二章 线程控制)
本章将讲解控制线程行为方面的详细内容,而前面的章节中使用的都是它们的默认行为
线程属性
pthread接口允许我们通过设置每个对象关联的不同属性来细调线程和同步对象的行为。
管理这些属性的函数都遵循相同的模式:
1.每个对象与自己类型的属性对象进行关联(线程与线程属性关联,互斥量与互斥量属性关联等)
2.有一个初始化函数,把属性设置为默认值
3.有一个销毁属性对象的函数
4.每个属性都有一个从属性对象中获取属性值的函数
5.每个属性都有一个设置属性值的函数
下面是线程属性pthread_attr_t的初始化跟销毁函数
#include <pthread.h> int pthread_attr_t_init(pthread_attr_t *attr); int pthread_attr_t_destroy(pthread_attr_t *attr);
下图总结了POSIX.1定义了线程的属性
每个属性都有相应的获取跟设置函数
如果在创建线程时不需要了解线程的终止状态,就可以修改pthread_attr_t结构中detachstate线程属性,让线程一开始就处于分离状态
detachstate可以设置成两个合法值之一:PTHREAD_CREATE_DETACHED和PTHREAD_CREATE_JOINABLE(默认)。
#include <pthread.h> int pthread_attr_getdetachstate(const pthread_attr_t *restrict attr,int *detachstate); int pthread_attr_setdetachstate(pthread_attr_t *attr,int *detachstate);
下面给出一个以分离状态创建线程的函数
1 #include "apue.h" 2 #include <pthread.h> 3 4 int 5 makethread(void *(*fn)(void *), void *arg) 6 { 7 int err; 8 pthread_t tid; 9 pthread_attr_t attr; 10 11 err = pthread_attr_init(&attr); 12 if (err != 0) 13 return(err); 14 err = pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); 15 if (err == 0) 16 err = pthread_create(&tid, &attr, fn, arg); 17 pthread_attr_destroy(&attr); 18 return(err); 19 }
同样的,可以使用下面函数对线程栈属性进行管理
#include <pthread.h> int pthread_attr_t_getstack(const pthread_attr_t *restrict attr,void **restrict stackaddr,size_t *restrict stacksize); int pthread_attr_t_setstack(pthread_attr_t *attr,void *stackaddr,size_t stacksize);
可以使用下面函数来设置和读取线程属性stacksize
#include <pthread.h> int pthread_attr_getstacksize(const pthread_attr_t *restrict attr,size_t *restrict stacksize); int pthread_attr_setstacksize(pthread_attr_t *attr,size_t stacksize);
线程属性guardsize控制着线程栈末尾之后用以避免栈溢出的扩展内存的大小。
#include <pthread.h> int pthread_attr_getguardsize(const pthread_attr_t *restrict attr,size_t *restrict guardsize); int pthread_attr_setguardsize(pthread_attr_t *attr,size_t guardsize);
同步属性
跟线程具有属性一样,线程的同步对象也有属性。本节将讨论互斥量属性、读写锁属性、条件变量属性和屏障属性。
互斥量属性
初始化跟反初始化函数
#include <pthread.h> int pthread_mutexattr_init(pthread_mutexattr_t *attr); int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);
值得注意的3个属性是:进程共享属性、健壮属性和类型属性。
下面两个函数用于设置跟获取进程共享属性
#include <pthread.h> int pthread_mutexattr_getpshared(const pthread_mutexattr_t *restrict attr,int *restrict pshared); int pthread_mutexattr_setpshared(pthread_mutexattr_t *attr,int pshared);
pshared可以设置为两个值:PTHREAD_PROCESS_PRIVATE(默认),PTHREAD_PROCESS_SHARED(从多个进程彼此之间共享的内存数据块中分配的互斥量就可以用于这些线程的同步)
下面两个函数用于设置跟获取健壮属性
#include <pthread.h> int pthread_mutexattr_getrobust(const pthread_mutexattr_t *restrict attr,int *restrict robust); int pthread_mutexattr_setrobust(pthread_mutexattr_t *attr,int robust);
健壮性属性取值有两种可能:
默认是PTHREAD_MUTEX_STALLED,这意味着持有互斥量的进程终止时不需要采取特别的动作;
另一个取值是PTHREAD_MUTEX_ROBUST,这个值导致线程调用pthread_mutex_lock获取锁,而该锁被另一个进程持有,但它终止时并没有对该锁进行解锁,此时线程会阻塞,从pthread_mutex_lock返回的值是EOWNERDEAD而不是0。
使用下面两个函数可以设置跟获取互斥量类型属性
#include <pthread.h> int pthread_mutexattr_gettype(const pthread_mutexattr_t *restrict attr,int *restrict type); int pthread_mutexattr_settype(pthread_mutexattr_t *attr,int type);
读写锁属性
初始化跟反初始化函数
#include <pthread.h> int pthread_rwlockattr_init(pthread_rwlockattr_t *attr); int pthread_rwlockattr_destroy(pthread_rwlockattr_t *attr);
读写锁支持的唯一属性是进程共享属性。它与互斥量的进程共享属性是相同的。
#include <pthread.h> int pthread_rwlockattr_getpshared(const pthread_rwlockattr_t *restrict attr,int *restrict pshared); int pthread_rwlockattr_setpshared(pthread_rwlockattr_t *attr,int pshared);
条件变量属性
有一对函数用于初始化和反初始化条件变量属性
#include <pthread.h> int pthread_condattr_init(pthread_condattr_t *attr); int pthread_condattr_destory(pthread_condattr_t *attr);
目前定义了条件变量的两个属性:进程共享属性和时钟属性
与其他的同步属性一样,条件变量支持进程共享属性,下面两个函数用于设置跟获取进程共享属性
#include <phread.h> int pthread_condattr_getpshared(const pthread_condattr_t *restrict attr,int *restrict pshared); int pthread_condattr_setpshared(pthrea_condattr_t *attr,int *pshared);
时钟属性控制计算pthread_cond_timedwait函数的超时参数采用的是哪个时钟,合法值是下图列出的时钟ID
下面两个函数用于设置跟获取时钟属性
#include <pthread.h> int pthread_condattr_getclock(const pthread_condattr_t *restrict attr,clockid_t *restrict clock_id); int pthread_condattr_setclock(pthread_condattr_t *attr,clockid_t *clock_id);
屏障属性
#include <pthread.h> int pthread_barrierattr_init(pthread_barrierattr_t *attr); int pthread_barrierattr_destroy(pthread_barrierattr_t *attr);
目前定义的屏障属性只有进程共享属性,作用与其他同步对象一样
#include <pthread.h> int pthread_barrierattr_getpshared(const pthread_barrierattr_t *restrict attr,int *restrict pshared); int pthread_barrierattr_setpshared(pthread_barrierattr_t *attr,int pshared);
重入
如果一个函数在相同的时间点可以被多个线程安全地调用,就称该函数是线程安全的。下图列出POSIX.1中不能保证线程安全的函数。
支持线程安全函数的操作系统实现会在<unistd.h>中定义符号_POSIX_THREAD_SAFE_FUNCTIONS,
对POSIX.1中的一些非线程安全函数,它会提供可替代的线程安全版本。下面列出这些函数的线程安全版本。
POSIX.1还提供了以线程安全的方式管理FILE对象的方法。可以使用flockfile和ftrylockfile获取给定FILE对象关联的锁。
这个锁是递归的:当你占有这把锁的时候,还是可以再次获取该锁,而且不会导致死锁。
#include <stdio.h> int ftrylockfile(FILE *fp); void flockfile(FILE *fp); void funlockfile(FILE *fp);
下面显示了getenv的一个非可重入版本
1 #include <limits.h> 2 #include <string.h> 3 4 #define MAXSTRINGSZ 4096 5 6 static char envbuf[MAXSTRINGSZ]; 7 8 extern char **environ; 9 10 char * 11 getenv(const char *name) 12 { 13 int i, len; 14 15 len = strlen(name); 16 for (i = 0; environ[i] != NULL; i++) { 17 if ((strncmp(name, environ[i], len) == 0) && 18 (environ[i][len] == '=')) { 19 strncpy(envbuf, &environ[i][len+1], MAXSTRINGSZ-1); 20 return(envbuf); 21 } 22 } 23 return(NULL); 24 }
如果两个线程同时调用这个函数,将会看到不一致的结果。
下面演示getenv的可重入版本
1 #include <string.h> 2 #include <errno.h> 3 #include <pthread.h> 4 #include <stdlib.h> 5 6 extern char **environ; 7 8 pthread_mutex_t env_mutex; 9 10 static pthread_once_t init_done = PTHREAD_ONCE_INIT; 11 12 static void 13 thread_init(void) 14 { 15 pthread_mutexattr_t attr; 16 17 pthread_mutexattr_init(&attr); 18 pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE); 19 pthread_mutex_init(&env_mutex, &attr); 20 pthread_mutexattr_destroy(&attr); 21 } 22 23 int 24 getenv_r(const char *name, char *buf, int buflen) 25 { 26 int i, len, olen; 27 28 pthread_once(&init_done, thread_init); 29 len = strlen(name); 30 pthread_mutex_lock(&env_mutex); 31 for (i = 0; environ[i] != NULL; i++) { 32 if ((strncmp(name, environ[i], len) == 0) && 33 (environ[i][len] == '=')) { 34 olen = strlen(&environ[i][len+1]); 35 if (olen >= buflen) { 36 pthread_mutex_unlock(&env_mutex); 37 return(ENOSPC); 38 } 39 strcpy(buf, &environ[i][len+1]); 40 pthread_mutex_unlock(&env_mutex); 41 return(0); 42 } 43 } 44 pthread_mutex_unlock(&env_mutex); 45 return(ENOENT); 46 }
线程特定数据
线程特定数据,也称为线程私有数据,是存储和查询某个特定线程相关数据的一种机制。
在分配线程特定数据之前,需要创建与该数据关联的键。这个键将用于获取对线程特定数据的访问。使用pthread_key_create创建一个键
#include <pthread.h> int pthread_key_create(pthread_key_t *keyp,void (*destructor)(void *));
创建的键存储在keyp指向的内存单元中,这个键可以被进程中的所有线程使用,但每个线程把这个键与不同的线程特定数据地址进行关联。
创建新建时,每个线程的数据地址设为空值。
pthread_key_create可以为该键关联一个可选择的析构函数。当这个线程退出时,如果数据地址已经被置为非空值,那么析构函数就会被调用。
线程通常使用malloc为线程特定数据分配内存。析构函数通常释放已分配的内存。
对于所有的线程,我们都可以通过调用pthread_key_delete来取消键与线程特定数据值之间的关联关系。
#include <pthread.h>
int pthread_key_delete(pthread_key_t key);
键一旦创建以后,就可以通过pthread_setspecific函数把键和线程特定数据关联起来,可以通过pthread_getspecific函数获得线程特定数据的地址。
#include <pthread.h> void *pthread_getspecific(pthread_key_t key); int pthread_setspecific(pthread_key_t key,const void *value);
下面演示使用线程特定数据来维护每个线程的数据缓冲区副本的getenv可重入实现。
1 #include <limits.h> 2 #include <string.h> 3 #include <pthread.h> 4 #include <stdlib.h> 5 6 #define MAXSTRINGSZ 4096 7 8 static pthread_key_t key; 9 static pthread_once_t init_done = PTHREAD_ONCE_INIT; 10 pthread_mutex_t env_mutex = PTHREAD_MUTEX_INITIALIZER; 11 12 extern char **environ; 13 14 static void 15 thread_init(void) 16 { 17 pthread_key_create(&key, free); 18 } 19 20 char * 21 getenv(const char *name) 22 { 23 int i, len; 24 char *envbuf; 25 26 pthread_once(&init_done, thread_init); 27 pthread_mutex_lock(&env_mutex); 28 envbuf = (char *)pthread_getspecific(key); 29 if (envbuf == NULL) { 30 envbuf = malloc(MAXSTRINGSZ); 31 if (envbuf == NULL) { 32 pthread_mutex_unlock(&env_mutex); 33 return(NULL); 34 } 35 pthread_setspecific(key, envbuf); 36 } 37 len = strlen(name); 38 for (i = 0; environ[i] != NULL; i++) { 39 if ((strncmp(name, environ[i], len) == 0) && 40 (environ[i][len] == '=')) { 41 strncpy(envbuf, &environ[i][len+1], MAXSTRINGSZ-1); 42 pthread_mutex_unlock(&env_mutex); 43 return(envbuf); 44 } 45 } 46 pthread_mutex_unlock(&env_mutex); 47 return(NULL); 48 }
取消选项
有两个线程属性并没有包含在pthread_attr_t结构中,它们是可取消状态和可取消类型,这两个属性影响着线程在相应pthread_cancel函数调用时锁呈现的行为。
可取消状态属性可以是PTHREAD_CANCEL_ENABLE,也可以是PTHREAD_CANCLE_DISABLE。线程可以通过调用pthread_setcancelstate修改它的可取消状态。
#include <pthread.h> int pthread_setcancelstat(int state,int *oldstate);
pthread_cancle调用并不等待线程终止。在默认情况下,线程在取消请求发出以后还是继续运行,知道线程达到某个取消点。
取消点是线程检查它是否被取消的一个位置,如果取消了,则按照请求行事。POSIX.1保证在线程调用如下列出的任何函数时,取消点就会出现。
线程启动时默认的可取消状态时PTHREAD_CANCEL_ENABLE。当状态设为PTHREAD_CANCLE_DISABLE时,对pthread_cancel的调用并不会杀死线程。
取消请求对这个线程来说还处于挂起状态,当取消状态再次变为PTHREAD_CANCLE_ENABLE时,线程将在下一个取消点上对所有的取消请求进行处理。
可以调用pthread_testcancel函数在程序中添加自己的取消点。
#include <pthread.h> void pthread_testcancel(void);
上面描述的默认取消类型是推迟取消。可以通过调用pthread_setcanceltype来修改取消类型。
#include <pthread.h> int pthread_setcanceltype(int type,int *oldtype);
type参数可以是PTHREAD_CANCEL_DEFERRED(默认),也可以是PTHREAD_CANCEL_ASYNCHRONOUS(异步取消)。
如果使用异步取消。线程可以在任意时间撤销,而不是遇到取消点才能被取消。
线程和信号
信号的处理时进程中所有线程共享的。如果一个线程选择忽略某个给定信号,那么另一个线程就可以通过以下两种方式撤销上述线程的信号选择:
恢复信号的默认处理行为,或者为信号设置一个新的信号处理程序。
进程中的信号是递送给单个线程的。如果一个信号与硬件故障相关,那么该信号一般会被发送到引起该事件的线程中去,而其他的信号则被发送到任意一个线程。
第十章讨论了进程如何使用sigprocmask函数来阻止信号发送。而线程则必须使用pthread_sigmask。
#include <signal.h> int pthread_sigmask(int how,const sigset_t *restrict set,sigset_t *restrict oset);
pthread_sigmask函数与sigprocmask函数基本相同,不过pthread_sigmask失败时返回错误码,不再像sigprocmask函数那样设置errno并返回-1。
线程可以通过sigwait等待一个或多个信号的出现。
#include <signal.h> int sigwait(const sigset_t *restrict set,int *restrict signop); //发送信号的数量
要把信号发送给线程,可以调用pthread_kill。
#include <signal.h> int pthread_kill(pthread_t thread,int signo);
线程和fork
当线程调用fork时,就为整个子进程创建了整个进程地址空间的副本,子进程会从父进程那儿继承了每个互斥量、读写锁和条件变量的状态。
要清除锁状态,可以通过调用pthread_atfork函数建立fork处理程序。
#include <pthread.h> int pthread_atfork(void (*prepare)(void),void (*parent)(void),void (*child)(void));
prepare处理程序在父进程fork创建子进程前调用,作用是获取父进程定义的所有锁。
parent处理程序是创建子进程之后、返回之前在父进程上下文中调用的,作用是对获取的所有锁进行解锁。
child处理程序在fork返回之前在子进程上下文中调用,跟parent处理程序一样,作用是对获取的所有锁进行解锁。
下面程序描述了如何使用它pthread_atfork和fork处理程序
1 #include "apue.h" 2 #include <pthread.h> 3 4 pthread_mutex_t lock1 = PTHREAD_MUTEX_INITIALIZER; 5 pthread_mutex_t lock2 = PTHREAD_MUTEX_INITIALIZER; 6 7 void 8 prepare(void) 9 { 10 int err; 11 12 printf("preparing locks...\n"); 13 if ((err = pthread_mutex_lock(&lock1)) != 0) 14 err_cont(err, "can't lock lock1 in prepare handler"); 15 if ((err = pthread_mutex_lock(&lock2)) != 0) 16 err_cont(err, "can't lock lock2 in prepare handler"); 17 } 18 19 void 20 parent(void) 21 { 22 int err; 23 24 printf("parent unlocking locks...\n"); 25 if ((err = pthread_mutex_unlock(&lock1)) != 0) 26 err_cont(err, "can't unlock lock1 in parent handler"); 27 if ((err = pthread_mutex_unlock(&lock2)) != 0) 28 err_cont(err, "can't unlock lock2 in parent handler"); 29 } 30 31 void 32 child(void) 33 { 34 int err; 35 36 printf("child unlocking locks...\n"); 37 if ((err = pthread_mutex_unlock(&lock1)) != 0) 38 err_cont(err, "can't unlock lock1 in child handler"); 39 if ((err = pthread_mutex_unlock(&lock2)) != 0) 40 err_cont(err, "can't unlock lock2 in child handler"); 41 } 42 43 void * 44 thr_fn(void *arg) 45 { 46 printf("thread started...\n"); 47 pause(); 48 return(0); 49 } 50 51 int 52 main(void) 53 { 54 int err; 55 pid_t pid; 56 pthread_t tid; 57 58 if ((err = pthread_atfork(prepare, parent, child)) != 0) 59 err_exit(err, "can't install fork handlers"); 60 if ((err = pthread_create(&tid, NULL, thr_fn, 0)) != 0) 61 err_exit(err, "can't create thread"); 62 63 sleep(2); 64 printf("parent about to fork...\n"); 65 66 if ((pid = fork()) < 0) 67 err_quit("fork failed"); 68 else if (pid == 0) /* child */ 69 printf("child returned from fork\n"); 70 else /* parent */ 71 printf("parent returned from fork\n"); 72 exit(0); 73 }
线程与I/O
第三章介绍了pread函数和pwrite函数,它们使偏移量和数据的读写成为一个原子操作。