1

线程属性

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

  1. 线程和线程属性关联、互斥量和互斥量属性关联,一个属性对象可以代表多个属性

  2. 有一个初始化函数,并且可以将属性设置为默认值

  3. 有一个析构函数,能够销毁属性对象并且回收资源

  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,系统就会认为,我们将自己管理栈,从而会导致警戒缓冲区无效。但是苹果系统没有提供这个函数,应该是默认已经设置了一个默认值,或者说根本就没有设置警戒缓冲区。

同步属性

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

互斥量属性

互斥量属性使用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函数。这里不再赘述。


山河永寂
2.4k 声望159 粉丝