1.线程限制
用sysconf函数可以获得和thread相关的一些系统信息,主要是线程相关的一些最大值:
限量名 | 描述 | 名字参数 |
---|---|---|
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 |
虽然标准定义了这些常量,不过在很多系统上面可能根本就没有定义对应的限制符号(如_SC_THREAD_DESTRUCTOR_ITERATIONS可能未定义),或者sysconf函数返回错误。因此在很多时候这些很难派上用场
2.线程属性
1).前面讲到pthread_create等函数的时候,这些函数有一个参数pthread_attr_t。缺省情况下可以传NULL。但是如果想自己定义线程的相关属性的话,应该调用pthread_attr_init函数来定义:
#include <pthread.h> int pthread_attr_init(pthread_attr_t *attr); int pthread_attr_destroy(pthread_attr_t *attr); //成功返回0,失败返回错误号
pthread_attr_init函数负责初始化pthread_attr_t结构为缺省值。pthread_attr_destroy负责释放在pthread_attr_init函数调用时分配的内存,同时pthread_attr_destroy将会用无效值初始化属性对象,所以如果它被误用,pthread_create会返回一个错误。
2).
名字 | 描述 | FreeBSD 5.2.1 | Linux 2.4.22 | Mac OS X 10.3 | Solaris 9 |
---|---|---|---|---|---|
detachstate | 分离的线程属性 | * | * | * | * |
guardsize | 在线程栈末尾的保卫缓冲的字节尺寸 | * | * | * | |
stackaddr | 线程栈的最低地址 | ob | * | * | ob |
stacksize | 线程栈的字节尺寸 | * | * | * | * |
注:线程都拥有自己的栈
a.如果在创建这个线程的时候知道不需要线程的终止状态,,通过修改pthread_attr_t结构体里的detachstate属性,使线程以分离状态启动。可以使用pthread_attr_setdetachstate函数来设置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); //成功返回0,失败返回错误号。
可以调用pthread_attr_getdetachstate来得到当月的detachstate属性。被第二个参数指向的整型被设置为PTHREAD_CREATE_DETACHED或PTHREAD_CREATE_JOINABLE,取决于给定的pthread_attr_t结构体的这个属性的值。
b.查询和修改线程栈属性的一般通过较新的函数pthread_attr_getstack和pthread_attr_setstack来进行。这些函数去除了更老的接口定义里的歧义。
#include <pthread.h> int pthread_attr_getstack(const pthread_attr_t *restrict attr, void **restrict stackaddr, size_t *restric stacksize); int pthread_attr_setstack(pthread_attr_t *attr, void *statckaddr, size_t *stacksize); //两者成功返回0,失败返回错误号。
这两个函数可以用于管理stackaddr线程属性,也可以用于管理stacksize线程属性,如果线程栈用完了虚拟进程空间,那么调用malloc或mmap(14.9节)来为一个代替的栈分配空间,可以调用pthread_attr_setstack和pthread_attr_getstack来获得/设置线程的栈位置
c.stackaddr线程属性被定义为栈的内存单元的最低地址,但是不一定是栈的开始。可以用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_setstackszie(pthread_attr_t *attr, size_t stacksize); //两者成功返回0,失败返回错误号。
d.线程属性guardsize控制着线程末尾之后用以避免栈溢出的扩展内存的大小,缺省情况下这个大小正好是一个页=PAGESIZE。甚至可以用函数将该数值设置为0来禁止这个功能,如果我们修改了栈地址的话,系统会认为我们会自己处理溢出的问题,因此也不会提供这个功能。调用pthread_attr_get_guardsize和pthread_attr_set_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,失败返回错误号。
e.更多线程属性
如果具体操作系统实现是按照1对1,也就是一个用户模式线程对应一个内核模式线程的话,那么修改这个值没有作用。但是如果操作系统实现用少量内核模式线程/进程来模拟用户模式线程的话,那么修改这个值可能会提高或者降低程序和系统的性能。Level值并没有具体的意义,只是一个hint。Level=0表示让系统自动选择。pthread_setconcurrency函数可以提示系统,表明希望的并发度,函数原型如下:
#include <pthread.h> int pthread_getconcurrency(void); //返回当前并发级数。 int pthread_setconcurrency(int level); //成功返回0,失败返回错误号。
pthread_getconcurrency函数返回当前并发度。如果操作系统正控制着并发度(也就是说,之前没有调用过pthread_setconcurrency),那么pthread_getconcurrency将返回0
3.同步属性
就像线程具有属性一样,线程的同步对象也有属性,下面列出互斥量、读写锁和条件变量的属性
1).互斥量属性
a.使用pthread_mutexattr_init来初始化一个phread_mutexattr_t结构体,用pthread_mutexattr_destroy来对该结构进行销毁。
#include <pthread.h> int pthread_mutexattr_init(pthread_mutexattr_t *attr); int pthread_mutexattr_destroy(pthread_mutexattr_t *attr); //成功返回0,失败返回错误号。
b.可以使用pthread_mutexattr_getpshared函数来查询一个pthread_mutexattr_t结构体来得到进程共享属性。我们可以用pthread_mutexattr_setpshared函数改变进程共享属性。
#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库提供更高效的实现,当属性被设为PTHREAD_PROCESS_PRIVATE时,这是多线程应用的默认情况
c.可以用pthread_mutexattr_gettype来得到互斥量类型属性,pthread_mutexattr_settype来修改互斥量属性。
#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_MUTEX_NORMAL | 死锁 | 无定义 | 无定义 |
PTHREAD_MUTEX_ERRORCHECK | 返回错误 | 返回错误 | 返回错误 |
PTHREAD_MUTEX_RECURSIVE | 允许 | 返回错误 | 返回错误 |
PTHREAD_MUTEX_DEFAULT | 无定义 | 无定义 | 无定义 |
2).读写锁属性
读写锁和互斥量互斥,也有属性。用pthread_rwlockattr_init来初始化一个pthread_rwlockattr_t结构体,用pthread_rwlockattr_destroy来回收这个结构体。
#include <pthread.h> int pthread_rwlockattr_init(pthread_rwlockattr_t *attr); int pthread_rwlockattr_destroy(pthread_rwlockattr_t *attr); //两者成功返回0,失败返回错误号。
读写锁支持的唯一属性是进程共享属性,该属性与互斥量属性相同,就像互斥量的进程共享属性一样,用一对函数来读取和设置读写锁的进程共享属性
#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,失败返回错误号。
3).条件变量属性
条件变量也与属性,与互斥量和读写锁类似
#include <pthread.h> int pthread_condattr_init(pthread_condattr_t *attr); int pthread_condattr_destroy(pthread_condattr_t *attr); //两者成功返回0,失败返回错误号。
#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, 失败返回错误号。
4.重入
1).大部分Single UNIX Specification所定义的函数都是线程安全的,但是也有不少例外。实际使用的时候建议参考文档,确定函数是否是线程安全。
2.)文件支持用ftrylockfile, flockfile, funlockfile来锁定文件访问。标准IO函数被要求必须调用在内部实现中调用flockfile, funlockfile。基于字符的部分IO函数具有非线程安全版本,以_unlocked结尾,如:getchar_unlocked, getc_unlocked, putchar_unlocked, putc_unlocked
3).书中提供了一个可重入的getenv_r实现。要点是:
a.用到了Recursive Mutex(使用pthread_mutexattr_settype函数调用设置)来保护自己和其他线程冲突(普通的Mutex就可以做到),同时允许重入(必须用Recursive Mutex)
b.要求调用者提供自己的buffer,而不是用静态全局变量envbuf来访问结果
c.使用pthread_once函数保证只调用一个初始化函数一遍,用于初始化Mutex(当然用其他方法也可以)
5.线程私有数据
- 线程私有数据是一种很方便的将数据和线程联系起来的方法,在C Runtime中也大量用到线程私有数据来维护线程相关的数据,一个典型的例子是errno:实际上errno是一个函数调用,返回和线程相关的错误值。Windows中有类似的机制,称为TLS (Thread Local Storage)
- 访问线程私有数据需要使用Key。不同线程使用同一个key访问同一类型的数据(比如Errno),但是可以存放不同的值。Key的类型为pthread_key_t
- 用pthread_key_create函数创建key
1).在分配线程偶数据之前,需要创建与该数据相关联的键,这个键将用于获取对线程私有数据的访问权,使用pthread_key_create函数创建key
#include <pthread.h> int pthread_key_create(pthread_key_t *keyp, void (*destructor)(void *)); //成功返回0,失败返回错误号。
创建的关键字被存储在keyp所指的内存单元。这个键可以被进程里所有线程使用,但是每个线程将把这个键关联于一个不同的线程特定数据地址。当创建字被创建时,每个线程的数据地址被设为null值。
一般情况下,这个destructor用来销毁用户用malloc为线程私有数据分配的空间。注意:一般不应该用destructor来调用pthread_key_delete,因为delete对于一个key只用调一次,而destructor是对每个线程都调用的,前提是线程正常退出并且TSD不为NULL。
Key的总数量可能会有限制。可以用PTHREAD_KEYS_MAX来查询最大值。因为调用析构函数的时候这个析构函数可能又会创建新的Key,所以当线程退出的时候,调用Destructor的过程会反复继续直到没有key具有非NULL值或者次数到达最大值PTHREAD_DESTRUCTOR_ITERATIONS为止。这个值可以用sysconf获得。
2).对于所有线程,都可以通过调用pthread_key_delete来取消与线程私有数据值之间的关联
#include <pthread.h> int pthread_key_delete(pthread_key_t *key); //成功返回0,失败返回错误号。
注意调用pthread_key_delete并不会激活与键关联的析构函数,要释放任何与key对应的线程私有数据值的内存空间,需要在应用程序中采取额外的步骤。
3).有些线程可能看到一个关键字值,而另一个线程可能看到一个不同的值,这取决于系统如果调度线程。解决这个竞争的方法是使用pthread_once。
#include <pthread.h> pthread_once_t initflag = PTHREAD_ONCE_INIT; int pthread_once(pthread_once_t *initflag, void (*initfn)(void)); //成功返回0,失败返回错误码。
4).一旦关键被创建,就可以把线程私有数据关联到这个键,通过调用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,失败返回错误号。
如果没有线程私有数据与键关联,pthread_getspecific将返回一个空指针,可以使用它来确定是否需要调用pthread_setspecific
6.取消选项
有两个线程属性没有包含在pthread_attr_t结构体中,它们是取消状态和取消类型。这些属性影响了线程响应pthread_cancel所呈现的调用行为。
1).可取消状态属性可以是PTHREAD_CANCEL_ENABLE或PTHREAD_CANCEL_DISABLE。通过调用pthread_setcancelsatate改变它的可取消状态
#include <pthread.h> int pthread_setcancelstate(int state, int *oldstate); //成功返回0,失败返回错误号。
pthread_setcancelstate把当前的可取消状态设置为state,把原来的可取消状态放在由oldstate所指的内存单元,这是一个原子操作
2).可以调用pthread_testcancel来在你的程序里加入的取消点。
#include <pthread.h> void pthread_testcancel(void);
调用pthread_testcancel时,如果有某个和取消请求正处于未决状态,且取消没有置为无效,那么线程将被取消。但是如果取消被置为无效时,那么调用pthread_testcancel没有任何效果。
3).调用pthread_setcanceltype来改变取消类型
#include <pthread.h> int pthread_setcanceltype(int type, int *oldtype); //成功返回0,失败返回错误号。
type参数可以是PTHREAD_CANCEL_DEFERRED或PTHREAD_CANCEL_ASYNCHRONOUS。
7.线程和信号
与前面的进程信号类似
1).pthread_sigmask函数可以阻止Signal的发送:
#include <siganl.h> int pthread_sigmask(int how, const sigset_t *restrict set, sigset_t *restrict oset); //成功返回0,失败返回错误号。
2).调用sigwait函数可以等待signal的产生:
#include <signal.h> int sigwait(const sigset_t *restrict set, int *restrict signop); //成功返回0,失败返回错误号。
当signal是pending的情况下,调用sigwait会立刻返回并且把signal从pending list中移走,这样这个signal就不会被调用。为了避免这种行为,可以将pthread_sigmask和sigwait合用,首先用pthread_sigmask在signal产生之前阻止某个signal,然后用sigwait等待这个signal。Sigwait会自动Unblock这个signal,然后在等待结束之后恢复mask。
3).调用pthread_kill可以给一个线程发送signal:
#include <signal.h> int pthread_kill(pthread_t thread, int signo); //成功返回0,失败返回错误码。
8.线程和fork
当线程调用fork的时候,整个进程的地址空间都被copy(严格来说是copy-on-write)到子进程。所有互斥量Mutex / 读写锁Reader-Writer Lock / 信号量Condition Variable的状态都被继承下来。子进程中,只存在一个线程,就是当初调用fork的进程的拷贝。由于不是所有线程都被copy,因此需要将所有的同步对象的状态进行处理。(如果是调用exec函数的话没有这个问题,因为整个地址空间被丢弃了)处理的函数是pthread_atfork:
#include <pthread.h> int pthread_atfork(void (*prepare)(void), void (*parent)(void), void (*child)(void)); //成功返回0,失败返回错误号。
父进程和子进程最后解锁存储在不同内存位置的复制的锁,好像以下的事件序列发生一样:
1).父进程申请它所有的锁;
2).子进程申请它所有的锁;
3).父进程释放它的锁;
4).子进程释放它的锁。