[单刷APUE系列]第十二章——线程控制

线程属性

在前一章中,都是使用的函数默认的属性来赋予线程,可是pthread容许咱们经过设置对象关联的不一样属性来细调线程和同步对象的行为。而管理这些属性的函数基本都是形式相同的。安全

  1. 线程和线程属性关联、互斥量和互斥量属性关联,一个属性对象能够表明多个属性多线程

  2. 有一个初始化函数,而且能够将属性设置为默认值异步

  3. 有一个析构函数,可以销毁属性对象而且回收资源ide

  4. 每一个属性都有一个从属性对象中获取属性值的函数函数

  5. 每一个属性都有一个设置属性值的函数测试

还记得咱们在上一章中使用pthread_create函数的时候,传入的是null指针,函数就会使用默认值来设置线程,可是咱们也可使用pthread_attr_t结构体修改线程默认属性。操作系统

int pthread_attr_init(pthread_attr_t *attr);
int pthread_attr_destroy(pthread_attr_t *attr);

就和以前的线程构造析构函数同样,只不过产生的是线程属性对象而已。
在前面内容中讲到过度离线程的概念,若是咱们队线程的终止状态不感兴趣的话,就可使用分离线程让操做系统来对线程进行回收。可是,若是咱们是在建立线程的时候就须要分离线程,这就须要咱们使用pthread_attr_setdetachstate函数将其线程属性设置为PTHREAD_CREATE_DETACHED或者PTHREAD_CREATE_JOINABLE两种值,也就是分离和等待。线程

int pthread_attr_getdetachstate(const pthread_attr_t *attr, int *detachstate);
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);

SUS标准除了POSIX标准规定的之外,还有一些另外的属性,目前如今的Unix系统基本都是遵循这个标准的,因此线程栈也是一个重要属性。在编译阶段使用_POSIX_THREAD_ATTR_STACKADDR_POSIX_THREAD_ATTR_STACKSIZE符号来检查是否支持这两个线程栈属性,固然就像前面讲过的同样,咱们也可使用sysconf函数运行时检查。通常来讲,这编译时检查和运行时检查都是必须的,由于你不知道是否会出现跨平台编译。
在苹果系统平台下,并无发现存在pthread_attr_getstackpthread_attr_setstack函数,苹果系统将其分拆成了两个函数,也就是总共四个函数指针

int pthread_attr_getstackaddr(const pthread_attr_t *restrict attr, void **restrict stackaddr);
int pthread_attr_getstacksize(const pthread_attr_t *restrict attr, size_t *restrict stacksize);

int pthread_attr_setstackaddr(pthread_attr_t *attr, void *stackaddr);
int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);

进程咱们知道是有一个栈的,并且进程的虚拟内存地址空间是固定的,因此大小彻底无所谓,可是全部线程共享着同一个进程的地址空间,因此,若是线程栈累计大小超过了可用空间,就会致使溢出。
若是线程栈的虚地址空间用完了,可使用malloc或者mmap来分配空间,而且使用上面的函数改变新建线程的栈位置,stackaddr参数指定的是栈的最低内存地址.
线程属性guardsize控制着线程栈末尾后用于避免栈溢出的扩展内存大小。这个属性默认值是根据Unix具体实现的。能够把guardsize线程属性设置为0,不容许属性的这种特征行为发生,这就会致使警惕缓冲区不存在。一样,若是修改了stackaddr,系统就会认为,咱们将本身管理栈,从而会致使警惕缓冲区无效。可是苹果系统没有提供这个函数,应该是默认已经设置了一个默认值,或者说根本就没有设置警惕缓冲区。rest

同步属性

除了线程属性外,线程同步对象也有属性,就好比说各类锁

互斥量属性

互斥量属性使用pthread_mutexattr_t结构体,在前面的章节中,是使用PTHREAD_MUTEX_INITIALIZER常量或者用指向互斥量属性结构的空指针做为参数调用pthread_mutex_init函数。系统提供了相应的接口用于初始化。

int pthread_mutexattr_init(pthread_mutexattr_t *attr);
int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);

这两个函数就是初始化和反初始化函数,其中须要注意的就是:进程共享属性、健壮属性、以及类型属性。
进程中,咱们知道,多个线程能够访问同一个资源,可是进程访问同一个资源就须要设置PTHREAD_PROCESS_PRIVATE,或者说是进程共享互斥量属性。
Unix环境实际上有一种机制:容许独立的进程把同一个内存数据块映射到一个公共的地址空间中,而后多个进程就能访问共享的数据了,若是进程共享互斥量设置为PTHREAD_PROCESS_SHARED,多个进程彼此共享的内存数据块分配额互斥量就能够用于进程同步。

int pthread_mutexattr_getpshared(const pthread_mutexattr_t *restrict attr, int *restrict pshared);
int pthread_mutexattr_setpshared(pthread_mutexattr_t *attr, int pshared);

互斥量健壮属性和多个进程间共享的互斥量有关。这意味着,当持有互斥量的地址终止时,须要解决互斥量状态恢复问题

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这意味着进程终止时没有任何动做会采用,就可能致使等待着这个互斥量的其余进程处于等待状态。PHTREAD_MUTEX_ROBUST则是会对这个互斥量解锁。
很是遗憾的是,苹果没有对以上两种类型作出接口和支持,因此咱们也只能看看了。
类型互斥量属性控制着互斥量的锁定特性。POSIX标准规定了4中类型:

  1. PTHREAD_MUTEX_NORMAL 标准互斥量类型,不对其做任何的错误检查或者死锁检测

  2. PTHREAD_MUTEX_ERRORCHECK 提供了错误检查的类型

  3. PTHREAD_MUTEX_RECURSIVE 此类型容许同一线程在互斥量解锁以前对该互斥量进行屡次加锁,而且维护了锁的计数

  4. PTHREAD_MUTEX_DEFAULT 这个互斥量类型能够提供默认的行为和特性。操做系统在实现它的时候就是映射到其余互斥量的一种

int pthread_mutexattr_gettype(const pthread_mutexattr_t *restrict attr, int *restrict type);
int pthread_mutexattr_settype(pthread_mutexattr_t *attr, int type);

上面这两个函数就是设置和获取函数

读写锁属性

读写锁和互斥量很是类似,因此属性的函数也是基本差很少的,这里就随便的列举一下

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);

PTHREAD_PROCESS_SHARED Any thread of any process that has access to the memory where the read/write lock resides can manipulate the lock.
PTHREAD_PROCESS_PRIVATE Only threads created within the same process as the thread that initialized the read/write lock can manipulate the lock.  This is the default value.

条件变量属性

不用多说,先来两个初始化反初始化函数

int pthread_condattr_init(pthread_condattr_t *attr);
int pthread_condattr_destroy(pthread_condattr_t *attr);

条件变量只有两个属性:进程共享属性和时钟属性,可是好像苹果没有这两个属性。因此也就不讲了。
原著中的屏障属性,实际上在苹果系统下没有说明,因此也就略过了。

重入

因为线程是并行执行的,若是在同一时间点调用同一个函数,则有可能致使冲突,而若是一个函数在同一时间点能够被多个线程安全的调用,就称该函数是线程安全的。在Unix系统中,若是函数是线程安全的,就会在<unistd.h>中定义符号_POSIX_THREAD_SAFE_FUNCTIONS,固然也可使用sysconf函数获取限制。
对于非线程安全函数,系统会提供可替代的线程安全函数,这些函数只是在名字后面加上_r,表面是可重入函数。可是可重入不表明是异步安全的,由于信号处理函数在调用的时候可能会致使冲突。

线程指定数据

也成为线程私有数据,如同字面同样,线程将某些特定数据只容许自身查询。虽然线程模型轻量级的共享数据方式很是方便,可是也有着诸多弊端,因此引入了线程私有数据,用于维护基于线程的数据。例如errno,进程的errno不能共享给全部线程,因此后来就从新重构了errno的实现方式。
可是咱们知道,线程能访问进程全部地址空间,除了内核提供的寄存器等存储,线程理论上来讲不存在真正私有的线程数据。可是有一种机制约束也更加安全些。

int pthread_key_create(pthread_key_t *key, void (*destructor)(void *));

是否是很像键值对,没错,这就是键值对,建立的键储存在keyp指向的内存单元中,键能够被全部线程使用,可是每一个线程把这个键和不一样的线程特定数据地址关联。除了键之外还有一个析构函数,当线程退出时候,若是数据地址被置为非空值,那么析构函数就会被调用,咱们能够看到,它就一个无类型指针。
线程一般使用malloc为私有数据分配内存,析构函数就是释放已经分配的内存,若是不作析构,则会致使内存泄露。
线程退出时,线程特定数据的析构函数将会按照顺序被调用,当全部析构函数结束后,系统会检查是否还有非空线程特定数据与键关联,若是有,则再次调用析构函数。直到全部键为空。固然,也有最大尝试次数。

int pthread_key_delete(pthread_key_t key);

这个函数就是取消键和线程私有数据的关联。

取消选项

除了上面的属性外,实际上还有两个线程属性:可取消状态和可取消类型,这两个属性影响pthread_cancel函数行为。
可取消状态属性有两个值,PTHREAD_CANCEL_ENABLEPTHREAD_CANCEL_DISABLE,线程能够经过调用pthread_setcancelstate修改

int pthread_setcancelstate(int state, int *oldstate);

很是容易理解的函数,便可以用来查看旧状态也能够用于修改。
在前面章节pthread_cancel调用不会等待线程终止,而是等到一个取消检查点,统一检查状态,线程启动的时候默认为PTHREAD_CANCEL_ENABLE也就是接收取消,而为PTHREAD_CANCEL_DISABLE则不会杀死线程,只会阻塞这个请求,等状态再次变为接收而且到达下一个检查点的时候统一处理。
若是一直没有到达检查点,可能会致使取消的延迟,因此也提供了一个函数用于生成本身的检查点。

void pthread_testcancel(void);

在默认状况下,取消是延迟的,可是能够经过调用pthread_setcanceltype修改

int pthread_setcanceltype(int type, int *oldtype);

参数类型只有两种,PTHREAD_CANCEL_DEFERREDPTHREAD_CANCEL_ASYNCHRONOUS也就是异步和延迟。异步取消时,线程能够在任意时间撤销而不是到检查点。

线程和信号

在前面关于信号的章节,信号是基于进程的,而引入了线程以后,信号的处理就更加复杂了。每一个线程都有了本身的线程屏蔽字,可是信号的接收处理则是统一给进程的,当进程注册了信号处理函数后,全部线程的信号处理都会改变,可是进程中的信号是发送给单个线程的,一个线程能够修改撤销另外一个线程的信号选择,

int pthread_sigmask(int how, const sigset_t *restrict set, sigset_t *restrict oset);

这就是sigprocmask函数的pthread版本,也就是多线程版本。二者基本相同,不过pthread_sigmask则是工做在线程下。
为了简化信号处理,pthread提供了另外一个函数

int sigwait(const sigset_t *restrict set, int *restrict sig);

set参数指定了线程等待的信号集。返回的时候sig参数指向的内存将包含发送信号的数量。
这个函数的好处就在于简化了信号处理,将异步产生的信号经过阻塞的方式同步处理。一样的,因为线程的信号接收,也有了新的kill函数

int pthread_kill(pthread_t thread, int sig);

If sig is 0, error checking is performed, but no signal is actually sent.sig能够指定为0来测试线程的存在。

线程和fork

咱们讲过,当fork的时候,子进程会基本继承父进程的全部内容,也就是继承了全部互斥量、读写锁和条件变量。可是因为多线程的存在,fork后子进程必须清理锁的状态。
也许各位第一反应,是否是父子进程将会一样是多线程,实际上不是这样的,子进程只会包含父进程调用fork的线程的副本。因为子进程继承了锁,可是却没有继承占有锁的线程,因此须要清理锁,可是殊不知道清理哪些。
实际上,POSIX.1规定,在fork和第一个exec之间,子进程只能调用异步信号安全的函数,也就是限制了子进程“作什么”,而不是“如何清理”。

int pthread_atfork(void (*prepare)(void), void (*parent)(void), void (*child)(void));

为了确保清理锁,进程能够调用pthread_atfork函数注册清理函数,parent函数是fork建立子进程以后、返回以前在父进程中调用的,这是对全部锁进行解锁。child函数则是在fork返回以前,在子进程中调用。实际上二者解锁一样的内容,可是在不一样的进程中而已。

线程和I/O

多线程下全部线程共享一样的描述符,因此须要新的IO函数,而最简单的办法就是将其原子操做化,这样就不会出现IO的冲突。也就是pread和pwrite函数。这里再也不赘述。

相关文章
相关标签/搜索