引言
本章主要介绍线程属性、同步原语属性、同一进程中的多个线程之间如何保持数据私有性、进程如何与线程进行交互。
线程限制
下面是线程相关的一些限制:
| 限制名称 | 描述 | name参数 |
|---|---|---|
| PTHREAD_DESTRUCTOR_ITERATIONS | 线程退出时尝试销毁线程特定数据的最大次数 | _SC_THREAD_DESTRUCTOR_ITERATIONS |
| PTHREAD_KEYS_MAX | 进程可以创建的键的最大数目 | _SC_THREAD_KEYS_MAX |
| PTHREAD_STACK_MIN | 一个线程栈可用的最小字节数 | _SC_THREAD_STACK_MIN |
| PTHREAD_THREADS_MAX | 进程可以创建的最大线程数 | _SC_THREAD_THREADS_MAX |
下面描述了4种操作系统实现中线程限制的值,其中“没有确定限制”并不意味着值是无限的:
| 限制名称 | FreeBSD 8.0 | Linux 3.2.0 | Mac OS X 10.6.8 | Solaris 10 |
|---|---|---|---|---|
| PTHREAD_DESTRUCTOR_ITERATIONS | 4 | 4 | 4 | 没有确定限制 |
| PTHREAD_KEYS_MAX | 256 | 1024 | 512 | 没有确定限制 |
| PTHREAD_STACK_MIN | 2048 | 16384 | 8192 | 8192 |
| PTHREAD_THREADS_MAX | 没有确定限制 | 没有确定限制 | 没有确定限制 | 没有确定限制 |
线程属性
pthread接口允许我们通过关联的不同属性来细调线程和同步对象的行为。管理这些属性的行为有:
- 每个对象与它自己类型的属性对象进行关联(比如线程与线程属性关联,互斥量和互斥量属性关联),一个属性对象可以代表多个属性;
- 有一个初始化函数,把属性设置为默认值;
- 有一个销毁属性对象函数,销毁初始化函数分配的资源;
- 每个属性都有一个从属性对象中获取属性值的函数;
- 每个属性都有一个设置属性值的函数,属性值作为参数按值传递。
在pthread_create函数中,有一个参数是pthread_attr_t,它可以修改线程默认属性。可以使用pthread_attr_init初始化pthread_attr_t结构。在调用pthread_attr_init后,pthread_attr_t结构所包含的就是操作系统实现支持的所有线程属性的默认值。
1 |
|
pthread_attr_init初始化的属性对象是动态分配的,所以需要pthread_attr_destroy来释放这些内存空间。
分离线程:如果在创建线程时就知道不需要了解线程的终止状态,就可以修改pthread_attr_t 结构中detachstate线程属性,让线程一开始就处于分离状态。detachstate具有两个合法值:PTHREAD_CREATE_DETACHED——以分离状态启动线程、PTHREAD_CREATE_JOINABLE——正常启动线程,应用程序可以获取线程的终止状态。
1 |
|
线程栈,即为线程分配的栈。可以使用pthread_attr_getstack和pthread_attr_setstack对线程栈属性进行管理。
1 |
|
对于线程,虚地址空间的大小是固定的。但对于线程,同样大小的虚地址空间必须被所有的线程共享。如果使用许多线程,则这些线程栈累计大小就超过了可用的虚地址空间,就需要减少默认的线程栈大小。如果线程的函数分配了大量的自动变量,或调用函数设计很深的栈,则需要的栈比默认的大。
如果线程栈的虚地址空间消耗完了,则需要使用malloc或mmap来为可替代栈跟配空间,并用pthread_attr_setstack函数来改变新建线程的栈位置。stackattr参数指向线程栈的最低可寻址地址,该地址与边界地址对齐。当然,stackattr不一定是站的开始地址,如果一个处理器栈从高地址向低地址增长,那么stackaddr是线程栈的结尾位置。
应用程序可以通过pthread_attr_getstacksize和pthread_attr_setstacksize读取或设置线程属性stacksize。
1 |
|
其中,设置stacksize时其大小不能小于PTHREAD_STACK_MIN。
线程属性guardsize控制线程末尾用以避免栈溢出的扩展内存大小,默认值由具体实现决定,一般为系统页大小。将guardsize设置为0,则不会提供警戒缓冲区。如果程序修改了线程属性stackaddr,则系统认为由我们自己管理栈,栈警戒缓冲区机制无效,等同于将guardsize设置为0。
1 |
|
如果guardsize被修改,操作系统可能把它取为页大小的整数倍。如果线程的栈指针溢出至警戒区域,应用程序就可能通过信号接收到出错信息。
同步属性
和线程具有属性一样,线程同步的对象也有属性。
互斥量属性
互斥量属性用pthread_mutexattr_t结构表示。
在线程章节中提到,可以使用PTHREAD_MUTEX_INITIALIZER常量或NULL指针作为参数调用pthread_mutex_init得到互斥量的默认属性。
对于非默认属性,可以通过pthread_mutexattr_init初始化,pthread_mutexattr_destroy注销pthread_mutexattr_t结构。
1 |
|
对于互斥量属性,有3个值得注意:进程共享属性、健壮属性、类型属性。
在进程中,多个线程可以访问同一个同步对象,这是默认行为,这里可以将进程共享属性设置为PTHREAD_PROCESS_PRIVATE。
IPC存在一个机制:允许相互独立的多个进程将同一个内存数据块映射到它们独立的地址空间中。这是可以将进程共享互斥量属性设置为PTHREAD_PROCESS_SHARED,这样在多个进程共享的内存数据块中就可以将该互斥量用于进程间的同步。
pthread_mutexattr_getpshared函数可以查询pthread_mutexattr_t结构获取进程共享属性,pthread_mutexattr_setpshared函数可以修改进程共享属性。
1 |
|
互斥量健壮属性与多个进程间共享的互斥量有关。当持有互斥量的进程终止时,互斥量处于锁定状态,恢复起来会很困难。其他阻塞在这个锁的进程将会一直阻塞下去。
可以使用pthread_mutexattr_getrobust函数获取健壮的互斥量属性,可以使用pthread_mutexattr_setrobust函数设置健壮的互斥量属性值。
1 |
|
健壮属性有两种值
- PTHREAD_MUTEX_STALLED:默认值,表示使用互斥量后的行为是未定义,等待该互斥量解锁的程序会被拖住;
- PHTREAD_MUTEX_ROBUST:若线程调用pthread_mutex_lock获取锁,该锁被另一个进程持有,并且在进程终止时没有解锁,则线程阻塞,pthread_mutex_lock返回EOWNERDEAD,若有可能,不管互斥量的状态如何,都需要进行恢复。注意,EOWNERDEAD不是真正的错误,因为调用者将会拥有锁。
使用健壮属性,pthread_mutex_lock的返回值变成3个:不需要恢复的成功、需要恢复的成功、失败。
目前,只有Linux 3.2.0和Solaris 11支持健壮的线程互斥量
如果应用状态无法恢复,线程解锁互斥量后,该互斥量会进入永久不可用状态,线程可以调用pthread_mutex_consistent函数避免这个问题,保证该互斥量的状态与解锁前一致。
1 |
|
如果线程没有调用该函数对互斥量解锁,则其他试图获取该互斥量的阻塞线程会得到错误码ENOTRECOVERABLE,导致该互斥量不可再用。使用上述函数后,可以让互斥量正常工作,从而可以持续被使用。
类型互斥量属性控制互斥量的锁定特性,POSIX.1定义了4中类型。
- PTHREAD_MUTEX_NORMAL:默认类型,不做任何特殊的错误检查或死锁检测;
- PTHREAD_MUTEX_ERRORCHECK:提供错误检查;
- PTHREAD_MUTEX_RECURSIVE:允许同一线程在互斥量解锁之前多次对其加锁。使用递归互斥量维护锁的计数,在解锁次数和加锁次数不一致时,不会释放锁。
- PTHREAD_MUTEX_DEFAULT:提供默认特性和行为。操作系统可以将其自由映射为其他互斥量类型中的一种。Linux映射为普通互斥量类型,FreeBSD映射为错误检查类型。
上述4中类型行为如下所示(“不占用时解锁“表示一个线程对被另一个线程加锁的互斥量解锁;”在已解锁时解锁“表示一个线程对已解锁的互斥量进行解锁):
| 互斥量类型 | 没有解锁时重新加锁 | 不占用时加锁 | 在已解锁时解锁 |
|---|---|---|---|
| PTHREAD_MUTEX_NORMAL | 死锁 | 未定义 | 未定义 |
| PTHREAD_MUTEX_ERRORCHECK | 返回错误 | 返回错误 | 返回错误 |
| PTHREAD_MUTEX_RECURSIVE | 允许 | 返回错误 | 返回错误 |
| PTHREAD_MUTEX_DEFAULT | 未定义 | 未定义 | 未定义 |
可以使用pthread_mutexattr_gettype获取互斥量的类型属性。可以用pthread_mutexattr_settype修改互斥量类型属性。
1 |
|
读写锁属性
读写锁属性的数据类型为pthread_rwlockattr_t,可以用pthread_rwlockattr_init初始化,用pthread_rwlockattr_destroy反初始化该结构。
1 |
|
读写锁支持额唯一属性是进程共享属性。它与互斥量相同,它也有一对函数用于读取和设置读写锁的进程共享属性。
1 |
|
条件变量属性
Single UNIX Specification定义了两个条件变量属性:进程共享属性和时钟属性。同样,它们也有一对函数用于初始化和反初始化条件变量属性。
1 |
|
进程共享属性与其他同步属性一样,它控制了条件变量是可以被单进程的多个线程使用(PTHREAD_PROCESS_PRIVATE),还是多进程的多个线程使用(PTHREAD_PROCESS_SHARED)。同样有两个函数用于获取和设置进程共享属性。
1 |
|
时钟属性控制pthread_cond_timewait函数(用于条件变量中线程可以等待的时间)的超时参数tsptr采用哪个时钟,可以取下列中的时钟ID:
| 标识符 | 选项 | 说明 |
|---|---|---|
| CLOCK_REALTIME | 实时系统时间 | |
| CLOCK_MONOTONIC | _POSIX_MONOTONIC_CLOCK | 不带负跳数的实时系统时间 |
| CLOCK_PROCESS_CPUTIME_ID | _POSIX_CPUTIME | 调用进程的CPU时间 |
| CLOCK_THREAD_CPUTIME_ID | _POSIX_THREAD_CPUTIME | 调用线程的CPU时间 |
同样,有两个函数可以获取和设置时钟ID:
1 |
|
屏障属性
目前,屏障只有进程共享属性,同样,有两个函数可以对屏障的属性对象进行初始化和反初始化。
1 |
|
进程共享属性控制着屏障是可以被多进程的线程使用,还有被初始化屏障的进程中的多个线程使用。同样,对于该属性有两个函数可以获取和修改该属性。
1 |
|
重入
多个控制线程在相同时间可能调用相同的函数。如果一个函数在相同的时间点可以被多个线程安全调用,则称该函数是线程安全的。
很多函数不是线程安全的,因为它们通过静态的数据缓冲区存放返回的数据。可以要求调用者提供缓冲区来使函数变为线程安全。
一个线程安全的函数并不能说明它对信号处理程序是可重入的。如果函数对异步信号处理程序是可重入的,那么称该函数是异步信号安全的。
实例
函数getenv可以获取系统的环境变量,下面是不可重入版本的getenv,因为返回的字符串存放在一个静态缓冲区中。
1 | /* 获取环境变量 |
下面是可重入版本的getenv,这里使用了一个互斥量保护获取环境变量的过程。
1 | /* |
当然,即使getenv_r是线程安全的,但不代表它对信号处理程序是可重入的。如果使用费递归的互斥量,当信号处理程序中断线程执行,此时线程如果占有env_mutex,则其他试图获取该互斥量的线程会进入阻塞状态,从而导致死锁。
线程特定数据
线程特定数据(thread_specific data),也成为线程私有数据(thread-private data)。用于存储和查询线程特定相关数据的机制,每个线程都有自己单独的数据副本。使用线程特定数据的原因有两个:
- 需要维护基于单个线程的数据,防止某个线程的数据与其他线程的数据相混淆;
- 提供了基于进程的借口可以适应多线程环境机制,如errno,它就是线程私有数据,一个线程重置了errno的操作也不会影响其他线程的errno值。
在分配线程特定数据前,需要一个键与该数据关联,用于获取对线程特定数据的访问。使用pthread_key_create创建一个键。
1 |
|
keyp指向创建的键,键被一个进程中的所有线程使用,但每个线程可以将这个键与不同的线程特定数据相关联。创建新键时,每个线程的数据地址设为空值。
destructor表示与该键关联的析构函数。线程通常使用malloc为线程特定数据分配内存,析构函数用于释放该已分配的内存。调用析构函数的时机:
当线程退出后(调用pthread_exit或线程执行返回),如果数据地址是非空值,则会调用析构函数,它的唯一参数是该数据地址。
线程取消时,只有当最后的清理处理程序结束后,才会调用析构函数。
如果线程调用exit、_exit、_Exit或abort,或其他非正常退出时,就不会调用析构函数。
调用pthread_key_delete可以取消键与线程特定数据的关联关系。
1 |
|
调用pthread_key_delete不会调用与键关联的析构函数。
下面的代码会导致两个线程都调用pthread_key_create。
1 | void destructor(void *); |
解决上述问题的方法,可以使用pthread_once。
1 |
|
其中,initflag必须时非本地变量(可以是全局变量、静态变量),而且必须初始化为PTHREAD_ONCE_INIT。
每个线程调用pthread_once,系统就能保证initfn只被调用一次,解决上述问题的正确方式是:
1 | void destructor(void *); |
pthread_setspecific函数可以把键和线程特定数据相关联,pthread_getspecific函数可以获得线程特定数据的地址。
1 |
|
线程和fork
当线程调用fork,就为子进程创建了整个进程地址空间的副本,子进程还从父进程继承了每个互斥量、读写锁、条件变量的状态,因此子进程需要清理锁的状态。可以通过调用exec函数避免该问题,
exec函数会用磁盘上的新程序替换当前进程的正文段、数据段、堆段、栈段
旧的地址空间被抛弃,因此锁的状态也无关紧要,但如果子进程要继续执行老程序,则该方法行不通。
要清理锁的状态,可以通过pthread_atfork函数建立fork处理程序:
1 |
|
可以看到,pthread_atfork最多可以安装3个清理锁的程序。
- prepare:由父进程在fork创建子进程前调用,用于获取父进程定义的所有的锁;(即锁住所有锁)
- parent:在fork创建子进程之后,返回之前在父进程上下文中调用,用于对prepare获取的所有锁进行解锁;
- child:在fork创建子进程后,子进程返回之前在子进程上下文中调用,和parent一样,它负责释放prepare获取的所有锁。