APUE - 线程控制

线程属性

可以使用 pthread_attr_t 结构修改线程默认属性,并把这些属性与创建的线程联系起来。可以使用 pthread_attr_init 函数初始化 pthread_attr_t 结构。调用 pthread_attr_init 以后,pthread_attr_t 结构所包含的内容就是操作系统实现支持的线程所有属性的默认值。如果要修改其中个别属性的值,需要调用其它的函数。

#include <pthread.h>
int pthread_attr_init(pthread_attr_t *attr);
int pthread_attr_destroy(pthread_attr_t *attr);

如果要去除对 pthread_attr_t 结构的初始化,可以调用 pthread_attr_destroy 函数。如果 pthread_attr_init 实现时为属性对象分配了动态内存空间,pthread_attr_destroy 将会释放该内存空间。除此之外,pthread_attr_destroy 还会用无效的值初始化属性对象,因此如果该属性对象被误用,将会导致 pthread_create 函数返回错误。

如果在创建线程时就知道不需要了解线程的终止状态,则可以修改 pthread_attr_t 结构中的 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);

线程栈属性的查询和修改一般是通过 pthread_attr_getstack 和 pthread_attr_setstack 来进行。

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

这两个函数可以用于管理 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);

线程属性 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);

并发度控制着用户级线程可以映射的内核线程或进程的数目。pthread_setconcurrency 函数用于提示系统,表明希望的并发度。

#include <pthread.h>
int pthread_getconcurrency(void);
int pthread_setconcurrency(int level);

同步属性

互斥量属性

#include <pthread.h>
int pthread_mutexattr_init(pthread_mutexattr_t *attr);
int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);

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

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

读写锁属性

#include <pthread.h>
int pthread_rwlockattr_init(pthread_rwlockattr_t *attr);
int pthread_rwlockattr_destroy(pthread_rwlockattr_t *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);

条件变量属性

#include <pthread.h>
int pthread_condattr_init(pthread_condattr_t *attr);
int pthread_condattr_destroy(pthread_condattr_t *attr);

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

线程私有数据

线程私有数据是存储和查询与某个线程相关的数据的一种机制。

在分配线程私有数据之前,需要创建与该数据关联的键。这个键将用于获取线程私有数据的访问权。使用 pthread_key_create 创建一个键。

#include <pthread.h>
int pthread_key_create(pthread_key_t *keyp, void (*destructor)(void *) );

对所有的线程,都可以通过调用 pthread_key_delete 来取消键与线程私有数据值之间的关联关系。

#include <pthread.h>
int pthread_key_delete(pthread_key_t *key);

需要确保分配的键并不会由于在初始化阶段的竞争而发生变动。解决这种竞争的办法是使用 pthread_once。

#include <pthread.h>
pthread_once_t initflag = PTHREAD_ONCE_INIT;
int pthread_once(pthread_once_t *initflag, void (*initfn)(void) );

键一旦创建,就可以通过调用 pthread_setspecific 函数把键和线程私有数据关联起来。可以通过 pthread_getspecific 函数获得线程私有数据的地址。

#include <pthread.h>
void *pthread_getspecific(pthread_key_t key);
int pthread_setspecific(pthread_key_t key, const void *value);

取消选项

有两个线程属性并没有包含在 pthread_attr_t 结构中,它们是可取消状态和可取消类型。这两个属性影响着线程在响应 pthread_cancel 函数调用时所呈现的行为。

线程可以通过调用 pthread_setcancelstate 修改它的可取消状态。

#include <pthread.h>
int pthread_setcancelstate(int state, int *oldstate);

pthread_cancel 调用并不等待线程终止,在默认情况下,线程在取消请求发出以后还是继续运行,直到线程到达某个取消点。取消点是线程检查是否被取消并按照请求进行动作的一个位置。

可以调用 pthread_testcancel 函数在程序中自己添加取消点。

#include <pthread.h>
void pthread_testcancel(void);

调用 pthread_testcancel 时,如果有某个取消请求正处于未决状态,而且取消并没有置为无效,那么线程就会被取消。但是如果取消被置为无效时,pthread_testcancel 调用就没有任何效果。

这里所描述的默认取消类型也称为延迟取消。调用 pthread_cancel 以后,在线程到达取消点之前,并不会出现真正的取消。可以通过调用 pthread_setcanceltype 来修改可取消类型。

#include <pthread.h>
int pthread_setcanceltype(int type, int *oldtype);

异步取消与延迟取消不同,使用异步取消时,线程可以在任意时间取消,而不是非得遇到取消点才能被取消。

线程和信号

每个线程都有自己的信号屏蔽字,但是信号的处理是进程中所有线程共享的。这意味着尽管单个线程可以阻止某些信号,但当线程修改了与某个信号相关的处理行为以后,所有的线程都必须共享这个处理行为的改变。这样如果一个线程选择忽略某个信号,而其他的线程可以恢复信号的默认处理行为,或者为信号设置一个新的处理程序,从而可以撤销上述线程的信号选择。

进程中的信号是递送到单个线程的。如果信号与硬件故障或计时器超时相关,该信号就被发送到引起该事件的线程中去,而其他的信号则被发送到任意一个线程。

#include <signal.h>
int pthread_sigmask(int how, const sigset_t *restrict set,
                    sigset_t *restrict oset);

#include <signal.h>
int sigwait(const sigset_t *restrict set, int *restrict signop);

#include <signal.h>
int pthread_kill(pthread_t thread, int signo);

线程和 fork

当线程调用 fork 时,就为子进程创建了整个进程地址空间副本。子进程与父进程是完全不同的进程,只要两者都没有对内存做出改动,父进程和子进程之间还可以共享内存页的副本。

子进程通过继承整个地址空间的副本,也从父进程那里继承了所有互斥量、读写锁和条件变量的状态。如果父进程包含多个线程,子进程在 fork 返回以后,如果紧接着不是马上调用 exec 的话,就需要清理锁状态。

要清除锁状态,可以通过调用 pthread_atfork 函数建立 fork 处理程序。

#include <pthread.h>
int pthread_atfork(void (*prepare)(void), void (*parent)(void),
                   void (*child)(void) );

用 pthread_atfork 函数最多可以安装三个清理锁的函数。prepare fork 处理程序由父进程在 fork 创建子进程前调用,这个 fork 处理程序的任务是获取父进程定义的所有锁。parent fork 处理程序是在 fork 创建了子进程以后,但在 fork 返回之前在父进程环境中调用的,这个 fork 处理程序的任务是对 prepare fork 处理程序获得的所有锁进行解锁。child fork 处理程序在 fork 返回之前在子进程环境中调用,与 parent fork 处理程序一样,child fork 处理程序也必须释放 prepare fork 处理程序获得的所有锁。

线程和 I/O

可以使用 pread 和 pwrite 来解决并发线程对同一文件进行读、写操作的问题。