UNIX环境高级编程(12-线程控制)
本章详细介绍了线程属性和同步原语属性。最后讨论基于进程的系统调用如何与线程进行交互。
属性
可以通过对每个对象关联的不同属性来细调线程和同步对象的行为。管理这些属性的函数大概有以下几类:
- 初始化函数,负责给属性设置为默认值
- 销毁函数,负责释放初始化函数分配的资源
- 获取属性值的函数
- 设置属性值的函数
线程属性
-
初始化和销毁
// Both return: 0 if OK, error number on failure int pthread_attr_init(pthread_attr_t *attr); int pthread_attr_destroy(pthread_attr_t *attr);
destroy
函数除了释放资源外,还会用无效的值初始化属性对象,这样当线程创建函数误用该对象时,会返回错误信息。 -
分离状态属性
detachstate
// Both return: 0 if OK, error number on failure int pthread_attr_getdetachstate(const pthread_attr_t *restrict attr, int *detachstate); int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
该状态可以设置成
PTHREAD_CREATE_DETACHED
或PTHREAD_CREATE_JOINABLE
,分别表示以分离状态或正常方式启动线程。 -
线程栈的相关属性
// Both return: 0 if OK, error number on failure int pthread_attr_getstack(const pthread_attr_t *restrict attr, void **restrict stackaddr,size_t *restrict stacksize); int pthread_attr_setstack(pthread_attr_t *attr, void *stackaddr, size_t stacksize);
stackaddr
参数指定的是栈的最低内存地址。如果不想手动设定栈地址,可以通过下面的函数来仅指定栈大小。
// Both return: 0 if OK, error number on failure 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
控制线程栈末尾之后用以避免栈溢出的扩展内存的大小。当此值设置为0或者修改了线程属性stackaddr
后,系统不会提供警戒缓冲区。// Both return: 0 if OK, error number on failure 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);
同步属性
互斥量属性
// Both return: 0 if OK, error number on failure
int pthread_mutexattr_init(pthread_mutexattr_t *attr);
int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);
-
进程共享属性(process-shared)
默认情况下,仅相同进程的线程可以访问同一个同步对象(
PTHREAD_PROCESS_PRIVATE
),但是在某些情况下,需要多个进程访问同一个同步对象,这时候可以将属性设置为THREAD_PROCESS_SHARED
// Both return: 0 if OK, error number on failure int pthread_mutexattr_getpshared(const pthread_mutexattr_t * restrict attr, int *restrict pshared); int pthread_mutexattr_setpshared(pthread_mutexattr_t *attr,int pshared);
-
健壮属性(robust)
当某个线程在终止时没有释放持有的锁,那么当其他线程尝试获取该锁时,会发生问题。如果使用默认的设置(
PTHREAD_MUTEX_STALLED
),则请求的线程会一直阻塞。可以通过设置为PTHREAD_MUTEX_ROBUST
解决这个问题,此时lock函数的返回值为EOWNERDEAD
。如果线程加锁时发现返回值为
EOWNERDEAD
,那么在解锁前需要调用consistent函数,声明互斥量的一致性(与该互斥量相关的状态在互斥量解锁之前是一致的)。如果没有调用consistent函数就解锁,那么互斥量将不再可用,其他线程调用lock函数会返回ENOTRECOVERABLE
。// All return: 0 if OK, error number on failure int pthread_mutexattr_getrobust(const pthread_mutexattr_t *restrict attr, int *restrict robust); int pthread_mutexattr_setrobust(pthread_mutexattr_t *attr, int robust); int pthread_mutex_consistent(pthread_mutex_t * mutex);
-
类型属性(type)
控制互斥量的锁定特性。
- PTHREAD_MUTEX_NORMAL :标准互斥量,不进行错误检查或死锁检测。
- PTHREAD_MUTEX_ERRORCHECK :提供错误检查
- PTHREAD_MUTEX_RECURSIVE :允许同一线程在解锁前多次加锁。
- PTHREAD_MUTEX_DEFAULT :提供默认的特性和行为,操作系统可以将其映射为其他类型。
// Both return: 0 if OK, error number on failure int pthread_mutexattr_gettype(const pthread_mutexattr_t *restrict attr, int *restrict type); int pthread_mutexattr_settype(pthread_mutexattr_t *attr, int type);
Mutex type Relock without unlock? Unlock when not owned? Unlock when unlocked? PTHREAD_MUTEX_NORMAL deadlock undefined undefined PTHREAD_MUTEX_ERRORCHECK returns error returns error returns error PTHREAD_MUTEX_RECURSIVE allowed returns error returns error PTHREAD_MUTEX_DEFAULT undefined undefined undefined
读写锁属性
读写锁仅支持进程共享属性。
// All return: 0 if OK, error number on failure
int pthread_rwlockattr_init(pthread_rwlockattr_t *attr);
int pthread_rwlockattr_destroy(pthread_rwlockattr_t *attr);
int pthread_rwlockattr_getpshared(const pthread_rwlockattr_t *restrict attr,
int *restrict pshared);
int pthread_rwlockattr_setpshared(pthread_rwlockattr_t *attr,
int pshared);
条件变量属性
支持进程共享属性和时钟属性。
// All return: 0 if OK, error number on failure
int pthread_condattr_init(pthread_condattr_t *attr);
int pthread_condattr_destroy(pthread_condattr_t *attr);
int pthread_condattr_getpshared(const pthread_condattr_t *restrict attr,
int *restrict pshared);
int pthread_condattr_setpshared(pthread_condattr_t *attr, int pshared);
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);
时钟属性用于控制pthread_cond_timedwait
函数使用哪个系统时钟。
屏障属性
只有进程共享属性。
// All return: 0 if OK, error number on failure
int pthread_barrierattr_init(pthread_barrierattr_t *attr);
int pthread_barrierattr_destroy(pthread_barrierattr_t *attr);
int pthread_barrierattr_getpshared(const pthread_barrierattr_t *restrict attr,
int *restrict pshared);
int pthread_barrierattr_setpshared(pthread_barrierattr_t *attr, int pshared);
线程特定数据
线程模型促进了进程中数据和属性的共享,但是在部分场景下,我们又希望线程的部分数据可以是私有的。
一个进程中的所有线程都可以访问进程的整个地址空间,因此线程没有办法阻止另一个线程访问它的数据(除非使用寄存器),即使是接下来介绍的线程特定数据(thread-specific data)机制,也不能做到这一点。但是通过这种机制,可以提高线程间的独立性,使得线程不太容易访问到其他线程的线程特定数据。
每个线程通过键(key)来访问线程特定数据,键在进程中被所有线程使用,每个线程把自己的线程特定数据和键关联起来。这样,通过同一个键,每个线程可以管理与自己关联的数据。
// Both return: 0 if OK, error number on failure
int pthread_key_create(pthread_key_t *keyp, void (*destructor)(void *));
int pthread_key_delete(pthread_key_t key);
创建新键时,每个线程的数据地址为空。同时,在创建的时候可以指定一个析构函数,当线程退出时,如果数据地址不为空,则会调用这个析构函数(参数是数据地址)。
所有的线程都可以调用删除函数来取消键与数据之间的关联,但是这不会触发析构函数。
// Returns: thread-specific data value or NULL if no value has been associated with the key
void *pthread_getspecific(pthread_key_t key);
// Returns: 0 if OK, error number on failure
int pthread_setspecific(pthread_key_t key, const void *value);
我们可以通过get函数的返回值来确定是否需要调用set函数。
取消选项
有2个额外的线程属性并没有包含在上述的pthread_attr_t
中,它们分别是可取消状态和可取消类型。
可取消状态
该属性可以设置成PTHREAD_CANCEL_ENABLE
或PTHREAD_CANCEL_DISABLE
。
// Returns: 0 if OK, error number on failure
int pthread_setcancelstate(int state, int *oldstate);
set函数把当前的可取消状态设置为state
,同时将原来的状态通过oldstate
返回。
11章在介绍pthread_cancle
函数时,我们说到该函数仅仅是提出一个请求,而不保证线程被马上终止。在默认的情况下(即PTHREAD_CANCEL_ENABLE
),线程在取消请求发出后,在到达某个取消点时前,都会一直运行。
在线程调用某些函数时(函数列表见ch12/Cancellation points-x.png),取消点就会出现。但是对于部分特殊的线程,可能很长一段时间都不会调用到这些函数,那么可以使用pthread_testcancel
函数手动添加取消点。
void pthread_testcancel(void);
如果将状态设置为PTHREAD_CANCEL_DISABLE
,那么调用pthread_cancle
函数并不会杀死线程,取消请求会一直处于挂起状态,直到状态被设置为ENABLE。同理,此时调用pthread_testcancel
没有任何效果。
可取消类型
该属性可以设置成PTHREAD_CANCEL_DEFERRED
或PTHREAD_CANCEL_ASYNCHRONOUS
。
// Returns: 0 if OK, error number on failure
int pthread_setcanceltype(int type, int *oldtype);
默认设置为PTHREAD_CANCEL_DEFERRED
,即推迟取消,线程到达取消点之前不会被真正取消。如果设置为PTHREAD_CANCEL_ASYNCHRONOUS
,即异步取消,那么线程可以在任意时间撤销,而不必等待到达取消点。
信号
每个线程有自己的信号屏蔽字,通过pthread_sigmask
函数进行设置,参数与sigprocmask
类似。
#include <signal.h>
// Returns: 0 if OK, error number on failure
int pthread_sigmask(int how, const sigset_t *restrict set,
sigset_t *restrict oset);
需要注意的是,如果在主线程中屏蔽了一些信号,那么被创建的线程会继承当前的信号屏蔽字。
线程可以通过sigwait
函数等待一个或多个信号出现。如果多个线程通过该函数等待信号,则在传递信号的时候,只有一个线程可以从该函数返回。
// Returns: 0 if OK, error number on failure
int sigwait(const sigset_t *restrict set, int *restrict signop);
可以调用pthread_kill
函数将信号发送给指定的线程(需属于同一进程)。
// Returns: 0 if OK, error number on failure
int pthread_kill(pthread_t thread, int signo);
另外,如果传递给signo
的值是0,则可以用来检测线程是否存在。如果接收信号的线程没有对应的处理函数,则该信号会发送给主线程[1]。相关测试见ch12/pthread_kill.c,摘录主要代码如下:
int main()
{
int err;
sigset_t mask, old;
pthread_t pt1, pt2;
sigemptyset(&mask);
sigaddset(&mask, SIGQUIT); /* 如果不屏蔽QUIT信号,则主线程会收到该信号 */
sigaddset(&mask, SIGINT);
err = pthread_sigmask(SIG_BLOCK, &mask, &old);
assert(err == 0);
signal(SIGQUIT, main_q); /* QUIT信号处理函数 */
err = pthread_create(&pt1, NULL, th1, NULL);
assert(err == 0);
sleep(1);
printf("main:send QUIT signal.\n");
// 线程1未屏蔽QUIT信号,但没有处理程序,会返回给主线程
pthread_kill(pt1, SIGQUIT);
sleep(10);
return 0;
}
// 线程1
void* th1(void* a)
{
int err, signo;
sigset_t mask;
sigemptyset(&mask);
sigaddset(&mask, SIGINT);
pthread_sigmask(SIG_BLOCK, &mask, NULL);
while (1) {
err = sigwait(&mask, &signo);
assert(err == 0);
switch (signo) {
case SIGINT:
printf("\nth1:INT.\n");
break;
default:
printf("\nth1:unexcepted signal %d.\n", signo);
break;
}
}
}
在多线程中,一般安排专用线程处理信号,通过互斥量的保护,信号处理线程可以安全地改动数据。
fork
线程调用fork时,为子进程创建了整个进程地址空间的副本,同时还继承了互斥量、读写锁和条件变量的状态。为此,子进程返回后,如果不是马上调用exec,则需要清理锁的状态。因为子进程中只含有调用fork的那个线程的副本,父进程中其他占有锁的线程在子进程中不存在。
要清除锁的状态,可以使用pthread_atfork
函数建立fork处理程序。
// Returns: 0 if OK, error number on failure
int pthread_atfork(void (*prepare)(void), void (*parent)(void),
void (*child)(void));
prepare
由父进程在fork创建子进程前调用。任务是获取父进程定义的所有锁。parent
在fork创建子进程后、返回之前在父进程上下文中调用。任务是对获取的所有锁进行解锁。child
在fork返回前在子进程上下文中调用。任务是释放所有的锁。
可以多次调用该函数以设置多套fork处理程序。对于不需要的某个处理程序,可以传入空指针。多次调用时,parent
和child
以注册时的顺序执行,而prepare
的执行顺序与注册时相反。
使用方法参考ch12/pthread_atfork.c。