第十二章:线程控制

12.1、前言

本章将介绍同一进程的多个线程之间如何保持数据的私有性以及基于进程的系统调用如何与线程进行交互。

12.2、线程限制

在2.5.4节中讨论了sysconf函数,Single UNIX Specification定义了与线程操作有关的一些限制。与其他限制一样,这些限制可通过sysconf查询。这些限制的作用是为了增加应用程序在不同操作系统之间的可移植性。

12.3、线程属性

线程属性用pthread_attr_t结构来保存,可以将该类型的变量传递给线程创建函数以改变线程的默认属性。也可以调用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_t结构的初始化,可以调用pthread_attr_destroy函数。

11.5节中介绍了分离线程的概念。如果对现有的某个线程的中止状态不感兴趣的话,可以使用pthread_detach函数让操作系统在线程退出时收回它所占用的资源。

如果再创建线程时就知道不需要了解线程的中止状态,则可以修改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 *restirct attr, int *detachstate);
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
// 两者的返回值都是:若成功则返回0,否则返回错误编号

可以调用pthread_getdetachstate()函数获取当前detachstate线程属性,第二个参数所指的整数也许被设置为PTHREAD_CREATE_DETACHED,也可能被设置为PTHREAD_CREATE_JOINABLE,具体要取决于给定的pthread_attr_t结构中的属性值。

POSIX.1定义了线程栈属性的一些操作接口。

#include <pthread.h>
int pthread_attr_getstack(const pthread_attr_t *restirct attr, void **restirct stackaddr, size_t *restirct stacksize);
int pthread_attr_setstack(const pthread_attr_t *attr, void *stackaddr, size_t *stacksize);
// 两者的返回值都是:若成功则返回0,否则返回错误编号

这两个函数用户管理stackaddr属性,也可以用于管理satcksize线程属性。

应用程序也可以通过pthread_attr_getstacksize()和pthread_attr_setstacksize()函数读取或设置线程属性stacksize。

#include <pthread.h>
int pthread_attr_getstacksize(const pthread_attr_t *restirct attr, size_t *restirct stacksize);
int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);
// 两者的返回值都是:若成功则返回0,否则返回错误编号

如果希望改变栈的默认大小,但又不想自己处理线程栈的分配问题,这时使用pthread_attr_setstacksize()函数就非常有用。

线程属性guardsize控制着线程栈末尾之后用以避免栈溢出的扩展内存的大小。这个属性默认设置为PAGESIZE个字节。可以把guardsize设置为0,从而不允许属性的这种特征行为发生:在这种情况下不会提供警戒缓冲区。同样地,如果对线程属性stackaddr作了修改,系统就会假设我们自己管理栈,也会使guardsize失效,等同于设置为0.

#include <pthread.h>
int pthread_attr_getguardsize(const pthread_attr_t *restirct attr, size_t *restirct guardsize);
int pthread_attr_setguardsize(pthread_attr_t *attr, size_t guardsize);
// 两者的返回值都是:若成功则返回0,否则返回错误编号

如果guardsize线程属性被修改了,操作系统可能把它取为页大小的整数倍。如果线程的栈指针溢出到警戒区域,应用程序就可能通过信号接收到出错信息。

更多的线程属性

线程还有其他的一些属性,这些属性并没有在pthread_attr_t结构中表达:

* 可取消状态

* 可取消类型

* 并发度

并发度控制着用户级线程可以映射的内核线程或进程的数目。如果操作系统的实现在内核级的线程和用户级的线程之间保持一对一的映射,那么改变并发度并不会有什么效果,因为所有的用户级的线程都可能被调度到。但是,如果操作系统的实现让用户级线程到内核级线程或进程间的映射关系是多对一的话,那么在给定时间内增加可运行的用户级线程数,可能会改善性能。pthread_setconcurrency函数可以用于提示系统,表名希望的并发度。

#include <pthread.h>
int pthread_getconcurrency(void); // 返回值:当前的并发度
int pthread_setconcurrency(int level); // 返回值:若成功则返回0,否则返回错误编号

pthread_setconcurrency函数设定的并发度只是对系统的一个提示,系统并不保证请求的并发度一定会采用。如果希望系统自己决定并发度,则可以将level设为0,这样如果想撤销之前的设定,可以将level设为0。

12.4、同步属性

就像线程具有属性一样,线程的同步对象也有属性。本节讨论互斥量、读写锁和条件变量的属性。

1.互斥量属性

用pthread_mutexattr_init初始化pthread_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,否则返回错误编号

 需要注意的是进程共享属性和类型属性。

可以使用以下函数设置或查看共享属性;PTHREAD_PROCESS_PRIVATE表示进程私有,PTHREAD_PROCESS_SHARED表示进程间可以共享。

#include <pthread.h>
int pthread_mutexattr_getpshared(const pthread_mutexattr_t *restirct attr, int *restirct pshared);
int pthread_mutexattr_setshared(pthread_mutexattr_t *attr, int pshared);
// 返回值:若成功则返回0,否则返回错误编号

类型互斥量属性控制着互斥量的属性,POSIX.1定义了四种类型:PTHREAD_MUTEX_NORMAL是标准的互斥量类型;PTHREAD_MUTEX_RECURSIVE表示互斥量可以被重复加锁而不死锁(即递归锁);PTHREAD_MUTEX_ERRORCHECK提供错误检查;PTHREAD_MUTEX_DEFAULT表示可以请求默认语义,操作系统实现时将它映射为其他类型。

可以用下列两个函数获取或修改互斥量类型属性:

#include <pthread.h>
int pthread_mutexattr_gettype(const pthread_mutexattr_t *restirct attr, int *restirct type);
int pthread_mutexattr_settype(pthread_mutexattr_t *attr, int type);
// 两者的返回值都是:若成功则返回0,否则返回错误编号

2.读写锁属性

#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 *restirct attr, int *restirct 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_getshared(const pthread_condattr_t *restirct attr, int *restirct pshared);
int pthread_condattr_setshared(pthread_condattr_t *attr, int pshared);
// 两者的返回值都是:若成功则返回0,否则返回错误编号

12.5、重入

如果一个函数可以再同一时刻被多个线程安全的调用,就称该函数是线程安全的。

12.6、线程私有数据

线程私有数据(也称线程特定数据)是存储和查询与某个线程相关的数据的一种机制。把这种数据称为线程私有数据或线程特定数据的原因是,希望每个线程可以独立地访问数据副本,而不需要担心与其他的同步访问问题。

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

#include <pthread.h>
int pthread_key_create(pthread_key_t *keyp, void (*destructor)(void *));
// 返回值:若成功则返回0,否则返回错误编号

创建的键存放在keyp指向的内存单元中,这个键可以被进程中的所有线程使用,但每个线程把这个键与不同的线程私有数据地址进行关联,

除了创建键以外,pthread_key_create可以选择为该键关联析构函数,当线程退出时,如果数据地址已经被置为NULL数值,那么析构函数就会被调用,它唯一的参数就是该数据地址。

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

#include <pthread.h>
int pthread_key_delete(pthread_key_t *key);
// 返回值:若成功则返回0,否则返回错误编号

注意:调用pthread_key_delete并不会激活与键关联的析构函数,要释放任何与键对应的线程私有数据值的内存空间,需要在应用程序中采取额外的步骤。

需要确保分配的键不会由于在初始化阶段的竞争而发生变动。需要用下面的代码来实现:

#include <pthread.h>
pthread_once_t initflag = PTHREAD_ONCE_INIT;
int pthread_once(pthread_once_t *initflag, void(*initfn)(void));
// 返回值:若成功则返回0,否则返回错误编号

initflag必须是一个非本地变量(即全局变量或静态变量),而且必须初始化为PTHREAD_ONCE_INIT。

如果每个线程都调用pthread_once,系统就能保证初始化例程initflag只被调用一次,即在系统首次调用pthread_once时。创建键时避免出现竞争的一个恰当的方法可以描述为:

void destructor(void*);

pthread_key_t key;
pthread_once_t init_done = PTHREAD_ONCE_INIT;

void thread_init()
{
    error = pthread_key_create(&key, desctructor);
}

int threadfunc(void *arg)
{
    pthread_once(&init_done, thread_init);
}

键一旦创建,就可以通过调用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,否则返回错误编号

12.7、取消选项

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

可取消状态属性可以使PTHREAD_CANCEL_ENABLE,也可以是PTHREAD_CANCEL_DISABLE。线程可以通过调用pthread_setcancelstate修改它的可取消状态。

#include <pthread.h>
int pthread_setcancelstate(int state, int *oldstate);
// 返回值:若成功则返回0,否则返回错误编号

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

#include <pthread.h>
int pthread_setcanceltype(int type, int *oldtype);
// 返回值:若成功则返回0,否则返回错误编号

12.8、线程与信号

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

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

10.12节讨论了进程如何使用sigprocmask来阻止信号发送。sigprocmask的行为在多线程的进程中并没有定义,线程必须使用pthread_sigmask。

#include <signal.h>
int pthread_sigmask(int how, const sigset_t *restirct set, sigset_t *restirct oset);
// 返回值:若成功则返回0,否则返回错误编号

线程可以通过调用sigwait等待一个或多个信号发生。

#include <signal.h>
int sigwait(const sigset_t *restirct set, int *restirct signop);
// 返回值:若成功则返回0,否则返回错误编号

要把信号发送到进程,可以调用kill;要把信号发送到线程,可以调用pthread_kill。

#include <signal.h>
int pthread_kill(pthread_t thread, int signo);
// 返回值:若成功则返回0, 否则返回错误编号

12.9、线程与fork

当线程调用fork时,就为子进程创建了整个进程地址空间的副本。

在子进程内部只存在一个线程,它是由父进程中调用fork的线程的副本构成的。如果父进程中线程占有锁,子进程同样占有这些锁。问题时子进程并不包含占有锁的线程的副本,所以子进程没有办法知道它占有了哪些锁并且需要释放哪些锁。

如果子进程在从fork返回后马上调用某个exec函数,就可以避免这样的问题。但如果子进程继做以前的处理工作的话,这种方法就行不通,还需要使用其他的策略。

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

#include <pthread.h>
int pthread_atfork(void (*prepare)(void), void (*parent)(void), void (*child)(void));
// 返回值:若成功则返回0,否则返回错误编号

12.10、线程与I/O

在3.11节介绍了pread和pwrite函数,这些函数在多线程环境下是非常有帮助的,因为进程的所有线程共享相同的文件描述符。

使用pread,使偏移量的设定和数据的读取成为一个原子操作。

12.11、小结

在UNIX系统中,线程提供了分解并发任务的一种替代模型。线程促进了独立控制线程之间的共享,但也带来了它特有的同步问题。本章中,我们考查了如何调整线程和他们的同步原语,讨论了线程的可重入性,还学习了线程如何与其他面向进程的系统调用进行交互。

posted @ 2015-06-15 17:40  冷冰若水  阅读(254)  评论(0编辑  收藏  举报