12.1 引言
本章讲解控制线程行为方面的详细内容,介绍线程属性和同步原语属性。
12.2 线程限制
12.3 线程属性
线程属性对象用pthread_attr_t
结构表示,可以用这个结构修改线程默认属性,并把这些属性与创建的线程联系起来。
#include <pthread.h>
int pthread_attr_init(pthread_attr_t *attr);
int pthread_attr_destroy(pthread_attr_t *attr);
// 两个函数的返回值:若成功,返回0;否则,返回错误编号
pthread_attr_init
函数以系统默认线程属性初始化attr指向的结构;pthread_attr_destroy
函数反初始化attr指向的结构,如果pthread_attr_init
的实现对属性对象的内存空间是动态分配的,pthread_attr_destroy
就会释放该内存空间,同时以无效的值初始化属性对象;- 下图总结了POSIX.1定义的线程属性:
- 分离状态属性:detachstate
#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);
// 两个函数的返回值:若成功,返回0;否则,返回错误编号
pthread_attr_getdetachstate
函数获取当前的detachstate线程属性;pthread_attr_setdetachstate
函数设置detachstate线程属性;- detachstate的两个合法值:
- PTHREAD_CREATE_DETACHED:以分离状态启动线程,应用程序不可获取线程的终止状态;
- PTHREAD_CREATE_JOINABLE:正常启动线程,应用程序可以获取线程的终止状态。
- 线程栈属性:stackaddr和stacksize
#include <pthread.h>
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);
// 两个函数的返回值:若成功,返回0;否则,返回错误编号
- stackaddr线程属性被定义为栈的最低内存地址;
- stacksize线程属性被定义为栈的最小长度。
应用程序也可以通过pthread_attr_getstacksize
和pthread_attr_setstacksize
函数读取或设置线程属性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);
// 两个函数的返回值:若成功,返回0;否则,返回错误编号
- 设置stacksize属性时,选择的stacksize不能小于PTHREAD_STACK_MIN。
- 线程栈末尾警戒缓冲区大小属性: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);
// 两个函数的返回值:若成功,返回0;否则,返回错误编号
- guardsize属性默认值由具体实现来定义,常用值是系统页大小;
- 若把guardsize属性设置为0,则不会提供警戒缓冲区;
- 如果修改了线程属性stackaddr,会使栈警戒缓冲区机制无效,等同于把guardsize设置为0。
12.4 同步属性
12.4.1 互斥量属性
互斥量属性用pthread_mutexattr_t
结构表示。
#include <pthread.h>
int pthread_mutexattr_init(pthread_mutexattr_t *attr);
int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);
// 两个函数的返回值:若成功,返回0;否则,返回错误编号
pthread_mutexattr_init
函数以默认互斥量属性初始化attr;pthread_mutexattr_destroy
函数反初始化attr指向的结构。
- 进程共享属性:pshared
#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);
// 两个函数的返回值:若成功,返回0;否则,返回错误编号
pthread_mutexattr_getpshared
函数获取当前的pshared属性;pthread_mutexattr_setpshared
函数设置pshared属性;- pshared的两个合法值:
- PTHREAD_PROCESS_PRIVATE:默认行为,在同一进程中的多个线程可以访问同一个同步对象;
- PTHREAD_PROCESS_SHARED:不同进程中的线程可以访问同一个同步对象。
- 健壮属性:robust
互斥量健壮属性与在多个进程间共享的互斥量有关,这意味着,当持有互斥量的进程终止时,需要解决互斥量状态恢复的问题。
#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);
// 两个函数的返回值:若成功,返回0;否则,返回错误编号
pthread_mutexattr_getrobust
函数获取当前的robust属性;pthread_mutexattr_setrobust
函数设置robust属性;- robust的两个合法值:
- PTHREAD_MUTEX_STALLED:默认行为,持有互斥量的进程终止时不需要采取特别的动作,在这种情况下,使用互斥量的行为是未定义的,等待该互斥量解锁的应用程序会被有效地“拖住”;
- PTHREAD_MUTEX_ROBUST:这个值将导致线程调用
pthread_mutex_lock
获取锁,而该锁被另一个进程持有,但它终止时并没有对该锁进行解锁,此时线程会阻塞,从pthread_mutex_lock
返回的值EOWNERDEAD而不是0。应用程序可以通过这个特殊的返回值获知,若有可能,不管它们保护的互斥量状态如何,都需要进行恢复。
如果应用状态无法恢复,在线程对互斥量解锁以后,该互斥量将处于永久不可用状态。为了避免这样的问题,线程可以调用pthread_mutex_consistent
函数,指明与该互斥量相关的状态在互斥量解锁之前是一致的:
#include <pthread.h>
int pthread_mutex_consistent(pthread_mutex_t *mutex);
// 返回值:若成功,返回0;否则,返回错误编号
- 如果线程没有先调用
pthread_mutex_consistent
就对互斥量进行了解锁,那么其他试图获取该互斥量的阻塞线程就会得到错误码ENOTRECOVERABLE;如果发生这种情况,互斥量将不再可用; - 线程通过提前调用
pthread_mutex_consistent
,能让互斥量正常工作,这样它就可以持续被使用。
- 类型属性:type
类型互斥量属性控制着互斥量的锁定特性,POSIX.1定义了4种类型:
- PTHREAD_MUTEX_NORMAL:一种标准互斥量类型,不做任何特殊的错误检查或死锁检测;
- PTHREAD_MUTEX_ERRORCHECK:此互斥量类型提供错误检查;
- PTHREAD_MUTEX_RECURSIVE:此互斥量类型允许同一线程在互斥量解锁之前对该互斥量进行多次加锁。递归互斥量维护锁的计数,在解锁次数和加锁次数不相同的情况下,不会释放锁;
- PTHREAD_MUTEX_DEFAULT:此互斥量类型可以提供默认特性和行为。操作系统在实现它的时候可以把这种类型自由地映射到其他互斥量类型中的一种。
#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);
// 两个函数的返回值:若成功,返回0;否则,返回错误编号
pthread_mutexattr_gettype
函数获取当前的type属性;pthread_mutexattr_settype
函数设置type属性。
12.4.2 读写锁属性
读写锁属性用pthread_rwlockattr_t
结构表示。
#include <pthread.h>
int pthread_rwlockattr_init(pthread_rwlockattr_t *attr);
int pthread_rwlockattr_destroy(pthread_rwlockattr_t *attr);
// 两个函数的返回值:若成功,返回0;否则,返回错误编号
pthread_rwlockattr_init
函数以默认读写锁属性初始化attr;pthread_rwlockattr_destroy
函数反初始化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);
// 两个函数的返回值:若成功,返回0;否则,返回错误编号
12.4.3 条件变量属性
条件变量属性用pthread_condattr_t
结构表示。
#include <pthread.h>
int pthread_condattr_init(pthread_condattr_t *attr);
int pthread_condattr_destroy(pthread_condattr_t *attr);
// 两个函数的返回值:若成功,返回0;否则,返回错误编号
pthread_condattr_init
函数以默认条件变量属性初始化attr;pthread_condattr_destroy
函数反初始化attr指向的结构。
- 进程共享属性:pshared
进程共享属性控制着条件变量是可以被单进程的多个线程使用,还是可以被多进程的线程使用。
#include <pthread.h>
int pthread_condattr_getpshared(const pthread_condattr_t *restrict attr,
int *restrict pshared);
int pthread_condattr_setpshared(pthread_condattr_t *attr,
int pshared);
// 两个函数的返回值:若成功,返回0;否则,返回错误编号
pthread_condattr_getpshared
函数获取当前的pshared属性;pthread_condattr_setpshared
函数设置pshared属性。
- 时钟属性:clock_id
时钟属性控制pthread_cond_timedwait
函数的超时参数(tsptr)时采用的是哪个时钟,其合法值取自下图:
#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);
// 两个函数的返回值:若成功,返回0;否则,返回错误编号
pthread_condattr_getclock
函数获取时钟ID;pthread_condattr_setclock
函数修改时钟ID。
12.4.4 屏障属性
屏障属性用pthread_barrierattr_t
结构表示。
#include <pthread.h>
int pthread_barrierattr_init(pthread_barrierattr_t *attr);
int pthread_barrierattr_destroy(pthread_barrierattr_t *attr);
// 两个函数的返回值:若成功,返回0;否则,返回错误编号
pthread_barrierattr_init
函数对屏障属性对象进行初始化;pthread_barrierattr_destroy
函数对屏障属性对象进行反初始化。
目前定义的屏障属性只有进程共享属性,它控制着屏障是可以被多进程的线程使用,还是只能被初始化屏障的进程内的多线程使用,它有一对函数用于读取和设置进程共享属性:
#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);
// 两个函数的返回值:若成功,返回0;否则,返回错误编号
- 进程共享属性的值可以是:
- PTHREAD_PROCESS_SHARED:多进程中的多个线程可用;
- PTHREAD_PROCESS_PRIVATE:只有初始化屏障的那个进程内的多个线程可用。
12.5 重入
- 如果一个函数在相同的时间点可以被多个线程安全地调用,就称该函数是线程安全的;
- 如果一个函数对多个线程来说是可重入的,就说这个函数就是线程安全的;
- 如果函数对异步信号处理程序的重入是安全的,那么就可以说函数是异步信号安全的。
POSIX.1提供了以线程安全的方式管理FILE对象的方法,可以使用flockfile
和ftrylockfile
获取给定FILE对象关联的锁,这个锁是递归的:
#include <stdio.h>
int ftrylockfile(FILE *fp);
// 返回值:若成功,返回0;若不能获取锁,返回非0数值
void flockfile(FILE* fp);
void funlockfile(FILE* fp);
为了避免对每一个字符的读写操作进行获取锁和释放锁的动作,出现了不加锁版本的基于字符的标准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);
// 两个函数的返回值:若成功,返回c;若出错,返回EOF
12.6 线程特定数据
线程特定数据,也称为线程私有数据,是存储和查询某个特定线程相关数据的一种机制。管理线程特定数据的函数可以提高线程间的数据独立性,使得线程不太容易访问到其他线程的线程特定数据。
在分配线程特定数据之前,需要创建与该数据关联的键,这个键将用于获取对线程特定数据的访问, pthread_key_create
函数创建一个键:
#include <pthread.h>
int pthread_key_create(pthread_key_t *keyp, void (*destructor)(void *));
// 返回值:若成功,返回0;否则,返回错误编号
- 创建的键存储在keyp指向的内存单元中,这个键可以被进程中的所有线程使用,但每个线程把这个键与不同的线程特定数据地址进行关联;创建新键时,每个线程的数据地址设为空值;
pthread_key_create
可以为所创建的键关联一个可选择的析构函数,当这个线程退出时,如果数据地址已被置为非空值,那么析构函数就会被调用,它唯一的参数就是该数据地址。
可以调用pthread_key_delete
来取消键与线程特定数据值之间的关联关系:
#include <pthread.h>
int pthread_key_delete(pthread_key_t key);
// 返回值:若成功,返回0;否则,返回错误编号
- 调用
pthread_key_delete
并不会激活与键关联的析构函数。
需要确保分配的键并不会由于在初始化阶段的竞争而发生变动,解决这种竞争的办法是使用pthread_once
:
#include <pthread.h>
pthread_once_t initflag = PTHREAD_ONCE_INIT;
int pthread_once(pthread_once_t *initflag, void (*initfn)(void));
// 返回值:若成功,返回0;否则,返回错误编号
- initflag必须是一个非本地变量(如全局变脸或静态变量),而且必须初始化为PTHREAD_ONCE_INIT;
- 如果每个线程都调用
pthread_once
,系统就能保证初始化例程initfn只被调用一次,即系统首次调用pthread_once
时。
键一旦创建以后,就可以通过调用pthread_setspecific
函数把键和线程特定数据关联起来,通过调用pthread_getspecific
函数获得线程特定数据的地址:
#include <pthread.h>
void *pthread_getspecific(pthread_key_t key);
// 返回值:线程特定数据值;若没有值与该键关联,返回NULL
int pthread_setspecific(pthread_key_t key, const void *value);
// 返回值:若成功,返回0;否则,返回错误编号
12.7 取消选项
可取消状态和可取消类型这两个属性不包含在pthread_attr_t
结构中,它们影响着线程在响应pthread_cancel
函数调用时所呈现的行为。
- 可取消状态:state
#include <pthread.h>
int pthread_setcancelstate(int state, int *oldstate);
// 返回值:若成功,返回0;否则,返回错误编号
pthread_setcancelstate
把当前的可取消状态设置为state,把原来的可取消状态存储在由oldstate指向的内存单元,这两步是一个原子操作;pthread_cancel
调用并不等待线程终止,在默认情况下,线程在取消请求发出以后还是继续运行,直到线程到达某个取消点;取消点是线程检查它是否被取消的一个位置,如果取消了,则按照请求行事;- 可取消状态属性的两个可取值:
- PTHREAD_CANCEL_ENABLE:默认行为,可以取消;
- PTHREAD_CANCEL_DISABLE:对
pthread_cancel
的调用并不会杀死线程,取消请求对这个线程来说还处于挂起状态,当取消状态再次变为PTHREAD_CANCEL_ENABLE时,线程将在下一个取消点上对所有挂起的取消请求进行处理。
可以调用pthread_testcancel
函数在程序中添加自己的取消点:
#include <pthread.h>
void pthread_testcancel(void);
- 调用
pthread_testcancel
时,如果有某个取消请求正处于挂起状态,而且取消状态没有设置为无效,那么线程就会被取消; - 如果取消被设置为无效,
pthread_testcancel
调用没有任何效果。
- 可取消类型:type
#include <pthread.h>
int pthread_setcanceltype(int type, int *oldtype);
// 返回值:若成功,返回0;否则,返回错误编号
pthread_setcanceltype
函数把取消类型设置为type,把原来的取消类型返回到oldtype指向的整型单元;- 可取消类型属性的两个可取值:
- PTHREAD_CANCEL_DEFERRED:默认行为,推迟取消,调用
pthread_cancel
以后,在线程到达取消点之前,并不会出现真正的取消; - PTHREAD_CANCEL_ASYNCHRONOUS:异步取消,线程可在任意时间撤消,不是非得遇到取消点才能被取消。
- PTHREAD_CANCEL_DEFERRED:默认行为,推迟取消,调用
12.8 线程和信号
每个线程都有自己的信号屏蔽字,但是信号的处理是进程中所有线程共享的。
sigprocmask
的行为在多线程的进程中并没有定义,线程必须使用pthread_sigmask
函数来阻止信号发送:
#include <signal.h>
int pthread_sigmask(int how, const sigset_t *restrict set,
sigset_t *restrict oset);
// 返回值:若成功,返回0;否则,返回错误编号
- set参数包含线程用于修改信号屏蔽字的信号集;
- how参数的可取值:
- SIG_BLOCK:把信号集添加到线程信号屏蔽字中;
- SIG_SETMASK:用信号集替换线程的信号屏蔽字;
- SIG_UNBLOCK:从线程信号屏蔽字中移除信号集。
- 如果oset参数不为空,线程之前的信号屏蔽字就存储在它指向的sigset_t结构中;
- 可以通过把set参数设置为NULL,并把oset参数设置为sigset_t结构的地址,来获取当前的信号屏蔽字,这种情况中的how参数会被忽略。
线程可以通过调用sigwait
等待一个或多个信号的出现:
#include <signal.h>
int sigwait(const sigset_t *restrict set, int *restrict signop);
// 返回值:若成功,返回0;否则,返回错误编号
- set参数指定了线程等待的信号集;
- 返回时,signop指向的整数将包含所等待到的信号;
- 如果信号集中的某个信号在
sigwait
调用的时候处于挂起状态,那么sigwait
将无阻塞地返回,在返回之前,sigwait
将从进程中移除那些处于挂起等待状态的信号; - 线程在调用
sigwait
之前,必须阻塞那些它正在等待的信号,sigwait
函数会原子地取消信号集的阻塞状态。
要把信号发送给线程,可以调用pthread_kill
:
#include <signal.h>
int pthread_kill(pthread_t thread, int signo);
// 返回值:若成功,返回0;否则,返回错误编号
- 可以传一个0值的signo来检查线程是否存在;
- 如果信号的默认处理动作是终止该线程,那么把信号传递给某个线程仍然会杀死整个进程。
12.9 线程和fork
- 当线程调用
fork
时,就为子进程创建了整个进程地址空间的副本; - 子进程通过继承整个地址空间的副本,还从父进程那儿继承了每个互斥量、读写锁和条件变量的状态。如果父进程包含一个以上的线程,子进程在
fork
返回以后,如果紧接着不是马上调用exec
的话,就需要清理锁状态; - 在子进程内部,只存在一个线程,它是由父进程中调用
fork
的线程的副本构成的;如果父进程中的线程占有锁,子进程将同样占有这些锁;
要清除锁状态,可以通过调用pthread_atfork
函数建立fork处理程序:
#include <pthread.h>
int pthread_atfork(void (*prepare)(void), void (*parent)(void),
void (*child)(void));
// 返回值:若成功,返回0;否则,返回错误编号
- prepare fork处理程序由父进程在
fork
创建子进程前调用,这个fork处理程序的任务是获取父进程定义的所有锁; - parent fork处理程序是在
fork
创建子进程以后、返回之前在父进程上下文中调用的,这个fork处理程序的任务是对prepare fork处理程序获取的所有锁进行解锁; - child fork处理程序在
fork
返回之前在子进程上下文中调用,这个fork处理程序的任务是对prepare fork处理程序获取的所有锁进行解锁; - 不会出现加锁一次解锁两次的情况,因为子进程地址空间在创建时就得到了父进程定义的所有锁的副本。
12.10 线程和I/O
pread
、pwrite
函数原子性的定位并执行I/O。
12.11 实例代码
chapter12