线程属性
12.3 线程属性
pthread 接口允许我们通过设置每个对象关联的不同属性来细调线 程和同步对象的行为。通常,管理这些属性的函数都遵循相同的模式。
(1)每个对象与它自己类型的属性对象进行关联(线程与线程属
性关联,互斥量与互斥量属性关联,等等)。一个属性对象可以代表多 个属性。属性对象对应用程序来说是不透明的。这意味着应用程序并不 需要了解有关属性对象内部结构的详细细节,这样可以增强应用程序的 可移植性。取而代之的是,需要提供相应的函数来管理这些属性对象。
(2)有一个初始化函数,把属性设置为默认值。
(3)还有一个销毁属性对象的函数。如果初始化函数分配了与属 性对象关联的资源,销毁函数负责释放这些资源。
(4)每个属性都有一个从属性对象中获取属性值的函数。由于函 数成功时会返回0,失败时会返回错误编号,所以可以通过把属性值存 储在函数的某一个参数指定的内存单元中,把属性值返回给调用者。
(5)每个属性都有一个设置属性值的函数。在这种情况下,属性 值作为参数按值传递。
在第11章所有调用pthread_create函数的实例中,传入的参数都是空 指针,而不是指向pthread_attr_t结构的指针。可以使用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);
两个函数的返回值:若成功,返回0;否则,返回错误编号 如果要反初始化pthread_attr_t结构,可以调用pthread_attr_destroy函
数。如果pthread_attr_init的实现对属性对象的内存空间是动态分配的, pthread_attr_destroy就会释放该内存空间。除此之外, pthread_attr_destroy还会用无效的值初始化属性对象,因此,如果该属性 对象被误用,将会导致pthread_create函数返回错误码。
图 12-3 总结了 POSIX.1 定义的线程属性。POSIX.1 还为线程执行调 度(Thread Execution Scheduling)选项定义了额外的属性,用以支持实 时应用,但我们并不打算在这里讨论这些属性。图12-3同时给出了各个 操作系统平台对每个线程属性的支持情况。
介绍了分离线程的概念。如果对现有的某个线程的终止状态 不感兴趣的话,可以使用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 *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结构 中的属性值。
12.4
对于遵循POSIX标准的操作系统来说,并不一定要支持线程栈属 性,但是对于遵循Single UNIX Specification 中 XSI 选项的系统来说,支 持线程栈属性就是必需的。可以在编译阶段使用 _POSIX_THREAD_ATTR_STACKADDR和 _POSIX_THREAD_ATTR_STACKSIZE符号来检查系统是否支持每一个 线程栈属性。如果系统定义了这些符号中的一个,就说明它支持相应的 线程栈属性。或者,也可以在运行阶段把SC_THREAD_ATTR STACKADDR 和_SC_THREAD_ATTR_STACKSIZE 参数传给sysconf函 数,检查运行时系统对线程栈属性的支持情况。
可以使用函数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);
两个函数的返回值:若成功,返回0;否则,返回错误编号 对于进程来说,虚地址空间的大小是固定的。因为进程中只有一个 栈,所以它的大小通常不是问题。但对于线程来说,同样大小的虚地址 空间必须被所有的线程栈共享。如果应用程序使用了许多线程,以致这 些线程栈的累计大小超过了可用的虚地址空间,就需要减少默认的线程 栈大小。另一方面,如果线程调用的函数分配了大量的自动变量,或者调用的函数涉及许多很深的栈帧(stack frame),那么需要的栈大小可 能要比默认的大。
如果线程栈的虚地址空间都用完了,那可以使用malloc或者 mmap(见14.8节)来为可替代的栈分配空间,并用pthread_attr_setstack 函数来改变新建线程的栈位置。由stackaddr参数指定的地址可以用作线 程栈的内存范围中的最低可寻址地址,该地址与处理器结构相应的边界 应对齐。当然,这要假设malloc和mmap所用的虚地址范围与线程栈当前 使用的虚地址范围不同。
stackaddr线程属性被定义为栈的最低内存地址,但这并不一定是栈 的开始位置。对于一个给定的处理器结构来说,如果栈是从高地址向低 地址方向增长的,那么stackaddr线程属性将是栈的结尾位置,而不是开 始位置。
#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);
两个函数的返回值:若成功,返回0;否则,返回错误编号 如果希望改变默认的栈大小,但又不想自己处理线程栈的分配问
题,这时使用pthread_attr_setstacksize函数就非常有用。设置stacksize属性 时,选择的stacksize不能小于PTHREAD_STACK_MIN。
线程属性guardsize控制着线程栈末尾之后用以避免栈溢出的扩展内 存的大小。这个属性默认值是由具体实现来定义的,但常用值是系统页 大小。可以把guardsize线程属性设置为0,不允许属性的这种特征行为发 生:在这种情况下,不会提供警戒缓冲区。同样,如果修改了线程属性 stackaddr,系统就认为我们将自己管理栈,进而使栈警戒缓冲区机制无 效,这等同于把guardsize线程属性设置为0。
两个函数的返回值:若成功,返回0;否则,返回错误编号 如果希望改变默认的栈大小,但又不想自己处理线程栈的分配问
题,这时使用pthread_attr_setstacksize函数就非常有用。设置stacksize属性 时,选择的stacksize不能小于PTHREAD_STACK_MIN。
线程属性guardsize控制着线程栈末尾之后用以避免栈溢出的扩展内 存的大小。这个属性默认值是由具体实现来定义的,但常用值是系统页 大小。可以把guardsize线程属性设置为0,不允许属性的这种特征行为发 生:在这种情况下,不会提供警戒缓冲区。同样,如果修改了线程属性 stackaddr,系统就认为我们将自己管理栈,进而使栈警戒缓冲区机制无 效,这等同于把guardsize线程属性设置为0。
#include <pthread.h>
iint 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;否则,返回错误编号 如果guardsize线程属性被修改了,操作系统可能会把它取为页大小
的整数倍。如果线程的栈指针溢出到警戒区域,应用程序就可能通过信 号接收到出错信息。
Single UNIX Specification还定义了一些其他的可选线程属性供实时 应用程序使用,但在这里不讨论这些属性。
线程还有一些其他的pthread_attr_t结构中没有表示的属性:可撤销 状态和可撤销类型。我们将在12.7节中讨论它们。
12.4 同步属性
12.4.1 互斥量属性
互斥量属性是用pthread_mutexattr_t结构表示的。第11章中每次对互 斥量进行初始化时,都是通过使用PTHREAD_MUTEX_INITIALIZER常 量或者用指向互斥量属性结构的空指针作为参数调用pthread_mutex_init 函数,得到互斥量的默认属性。
对于非默认属性,可以用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_mutexattr_init 函数将用默认的互斥量属性初始化pthread_mutexattr_t结构。
值得注意的3个属性是:进程共享属性、健壮 属性以及类型属性。POSIX.1中,进程共享属性是可选的。可以通过检 查系统中是否定义了_POSIX_THREAD_PROCESS_SHARED 符号来判 断这个平台是否支持进程共享这个属性,也可以在运行时把 _SC_THREAD_PROCESS_SHARED 参数传给sysconf函数进行检查。
我们将在第14章和第15章中看到,存在这样的机制:允许相互独立 的多个进程把同一个内存数据块映射到它们各自独立的地址空间中。就 像多个线程访问共享数据一样,多个进程访问共享数据通常也需要同 步。如果进程共享互斥量属性设置为PTHREAD_PROCESS_SHARED, 从多个进程彼此之间共享的内存数据块中分配的互斥量就可以用于这些 进程的同步。
可以使用pthread_mutexattr_getpshared函数查询pthread_mutexattr_t结 构,得到它的进程共享属性,使用pthread_mutexattr_setpshared函数修改 进程共享属性。
#include <pthread.h>
int pthread_mutexattr_getpshared(const pthread_mutexattr_t
int *attr,
*restrict attr,
int *restrict pshared);
pthread_mutexattr_setpshared(pthread_mutexattr_t
int pshared);
两个函数的返回值:若成功,返回0;否则,返回错误编号 进程共享互斥量属性设置为PTHREAD_PROCESS_PRIVATE时,允 许pthread线程库提供更有效的互斥量实现,这在多线程应用程序中是默
认的情况。在多个进程共享多个互斥量的情况下, pthread线程库可以 限制开销较大的互斥量实现。
12.4.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 *
int *attr,
restrict attr,
int *restrict pshared); pthread_rwlockattr_setpshared(pthread_rwlockattr_t
int pshared);
两个函数的返回值:若成功,返回0;否则,返回错误编号
虽然POSIX只定义了一个读写锁属性,但不同平台的实现可以自由 地定义额外的、非标准的属性。
12.4.3 条件变量属性
Single UNIX Specification目前定义了条件变量的两个属性:进程共 享属性和时钟属性。与其他的属性对象一样,有一对函数用于初始化和 反初始化条件变量属性。
#include <pthread.h>
int pthread_condattr_init(pthread_condattr_t *attr); int pthread_condattr_destroy(pthread_condattr_t *attr);
两个函数的返回值:若成功,返回0;否则,返回错误编号 与其他的同步属性一样,条件变量支持进程共享属性。它控制着条
件变量是可以被单进程的多个线程使用,还是可以被多进程的线程使 用。要获取进程共享属性的当前值,可以用 pthread_condattr_getpshared 函数。设置该值可以用pthread_condattr_setpshared函数。
#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;否则,返回错误编号 时钟属性控制计算pthread_cond_timedwait函数的超时参数(tsptr)
时采用的是哪个时钟。合法值取自图 6-8 中列出的时钟 ID。可以使用 pthread_condattr_getclock 函数获取可被用于pthread_cond_timedwait 函数 的时钟 ID,在使用 pthread_cond_timedwait 函数前需要用pthread_condattr_t对象对条件变量进行初始化。可以用 pthread_condattr_setclock函数对时钟ID进行修改。
#include <pthread.h>
int pthread_condattr_getclock(const pthread_condattr_t *
restrict attr,
clockid_t *restrict clock_id);
int pthread_condattr_setclock(pthread_condattr_t *attr,
clockid_t clock_id);
两个函数的返回值:若成功,返回0;否则,返回错误编号 奇怪的是,Single UNIX Specification并没有为其他有超时等待函数
12.4.5屏障属性
屏障也有属性。可以使用pthread_barrierattr_init函数对屏障属性对象 进行初始化,用pthread_barrierattr_destroy函数对屏障属性对象进行反初 始化。
#include <pthread.h>
int pthread_barrierattr_init(pthread_barrierattr_t *attr); int pthread_barrierattr_destroy(pthread_barrierattr_t
*attr);
两个函数的返回值:若成功,返回0;否则,返回错误编号 目前定义的屏障属性只有进程共享属性,它控制着屏障是可以被多 进程的线程使用,还是只能被初始化屏障的进程内的多线程使用。与其
他属性对象一样,有一个获取属性值的函数 (pthread_barrierattr_getpshared)和一个设置属性值的函数 (pthread_barrierattr_ setpshared)。
#include <pthread.h>
int pthread_barrierattr_getpshared(const pthread_barrierattr_t *
restrict attr,
int *restrict pshared);
int pthread_barrierattr_setpshared(pthread_barrierattr_t
*attr,
int pshared);
的属性对象定义时钟属性。
两个函数的返回值:若成功,返回0;否则,返回错误编号 进程共享属性的值可以是 PTHREAD_PROCESS_SHARED(多进程中的多个线程可用),也可以是PTHREAD_PROCESS_PRIVATE(只有 初始化屏障的那个进程内的多个线程可用)。
12.4.6 重入
10.6节讨论了可重入函数和信号处理程序。线程在遇到重入问题时 与信号处理程序是类似的。在这两种情况下,多个控制线程在相同的时 间有可能调用相同的函数。
如果一个函数在相同的时间点可以被多个线程安全地调用,就称该 函数是线程安全的。在Single UNIX Specification中定义的所有函数中, 除了图12-9中列出的函数,其他函数都保证是线程安全的。另外, ctermid和tmpnam函数在参数传入空指针时并不能保证是线程安全的。 类似地,如果参数mbstate_t传入的是空指针,也不能保证wcrtomb和 wcsrtombs函数是线程安全的。
支持线程安全函数的操作系统实现会在<unistd.h>中定义符号 _POSIX_THREAD_SAFE_FUNCTIONS。应用程序也可以在sysconf函数 中传入_SC_THREAD_SAFE_FUNCTIONS参数在运行时检查是否支持 线程安全函数。在SUSv4之前,要求所有遵循XSI的实现都必须支持线程 安全函数,但是在SUSv4中,线程安全函数支持这个需求已经要求具体 实现考虑遵循POSIX。
操作系统实现支持线程安全函数这个特性时,对POSIX.1中的一些 非线程安全函数,它会提供可替代的线程安全版本。图12-10列出了这些 函数的线程安全版本。这些函数的命名方式与它们的非线程安全版本的 名字相似,只不过在名字最后加了_r,表明这些版本是可重入的。很多 函数并不是线程安全的,因为它们返回的数据存放在静态的内存缓冲区 中。通过修改接口,要求调用者自己提供缓冲区可以使函数变为线程安 全。
信号安全的。我们在10.6节中讨论可重入函数时,图10-4中的函数就是 异步信号安全函数。
除了图12-10中列出的函数,POSIX.1还提供了以线程安全的方式管 理FILE对象的方法。可以使用flockfile和ftrylockfile获取给定FILE对象关 联的锁。这个锁是递归的:当你占有这把锁的时候,还是可以再次获取 该锁,而且不会导致死锁。虽然这种锁的具体实现并无规定,但要求所 有操作 FILE 对象的标准 I/O 例程的动作行为必须看起来就像它们内部 调用了flockfile和funlockfile。
include <stdio.h>
int ftrylockfile(FILE *fp);
返回值:若成功,返回0;若不能获取锁,返回非0数值
void flockfile(FILE *fp);
void funlockfile(FILE *fp); 虽然标准的I/O例程可能从它们各自的内部数据结构的角度出发,
是以线程安全的方式实现的,但有时把锁开放给应用程序也是非常有用 的。这允许应用程序把多个对标准I/O函数的调用组合成原子序列。当 然,在处理多个FILE对象时,需要注意潜在的死锁,需要对所有的锁仔 细地排序。
如果标准I/O例程都获取它们各自的锁,那么在做一次一个字符的 I/O时就会出现严重的性能下降。在这种情况下,需要对每一个字符的 读写操作进行获取锁和释放锁的动作。为了避免这种开销,出现了不加 锁版本的基于字符的标准I/O例程。
include <stdio.h>
int getchar_unlocked(void); int getc_unlocked(FILE *fp);
12.8线程和信号
即使是在基于进程的编程范型中,信号的处理有时候也是很复杂 的。把线程引入编程范型,就使信号的处理变得更加复杂。
每个线程都有自己的信号屏蔽字,但是信号的处理是进程中所有线 程共享的。这意味着单个线程可以阻止某些信号,但当某个线程修改了 与某个给定信号相关的处理行为以后,所有的线程都必须共享这个处理 行为的改变。这样,如果一个线程选择忽略某个给定信号,那么另一个 线程就可以通过以下两种方式撤消上述线程的信号选择:恢复信号的默 认处理行为,或者为信号设置一个新的信号处理程序。
进程中的信号是递送到单个线程的。如果一个信号与硬件故障相 关,那么该信号一般会被发送到引起该事件的线程中去,而其他的信号 则被发送到任意一个线程。
12.9 线程和fork
当线程调用fork时,就为子进程创建了整个进程地址空间的副本。 回忆8.3节中讨论的写时复制,子进程与父进程是完全不同的进程,只要 两者都没有对内存内容做出改动,父进程和子进程之间还可以共享内存 页的副本。
子进程通过继承整个地址空间的副本,还从父进程那儿继承了每个 互斥量、读写锁和条件变量的状态。如果父进程包含一个以上的线程, 子进程在fork返回以后,如果紧接着不是马上调用exec的话,就需要清 理锁状态。
在子进程内部,只存在一个线程,它是由父进程中调用fork的线程 的副本构成的。如果父进程中的线程占有锁,子进程将同样占有这些 锁。问题是子进程并不包含占有锁的线程的副本,所以子进程没有办法 知道它占有了哪些锁、需要释放哪些锁。
如果子进程从fork返回以后马上调用其中一个exec函数,就可以避 免这样的问题。这种情况下,旧的地址空间就被丢弃,所以锁的状态无 关紧要。但如果子进程需要继续做处理工作的话,这种策略就行不通, 还需要使用其他的策略。
在多线程的进程中,为了避免不一致状态的问题,POSIX.1声明, 在fork返回和子进程调用其中一个exec函数之间,子进程只能调用异步 信号安全的函数。这就限制了在调用exec之前子进程能做什么,但不涉 及子进程中锁状态的问题。
要清除锁状态,可以通过调用pthread_atfork函数建立fork处理程 序。
#include <pthread.h>
int pthread_atfork(void (*prepare)(void), void (*parent) (void),
void (*child)(void));
返回值:若成功,返回0;否则,返回错误编号 用pthread_atfork函数最多可以安装3个帮助清理锁的函数。prepare
fork处理程序由父进程在fork创建子进程前调用。这个fork处理程序的任 务是获取父进程定义的所有锁。parent fork处理程序是在fork 创建子进 程以后、返回之前在父进程上下文中调用的。这个fork处理程序的任务 是对prepare fork处理程序获取的所有锁进行解锁。child fork处理程序在 fork返回之前在子进程上下文中调用。与parent fork处理程序一样,child fork处理程序也必须释放prepare fork处理程序获取的所有锁。
12.10 线程和I/O
3.11节介绍了pread和pwrite函数。这些函数在多线程环境下是非常 有用的,因为进程中的所有线程共享相同的文件描述符。
考虑两个线程,在同一时间对同一个文件描述符进行读写操作。
线程A 线程B lseek(fd, 300, SEEK_SET); lseek(fd, 700,
SEEK_SET);
read(fd, buf1, 100); read(fd, buf2,
100);
如果线程A执行lseek然后线程B在线程A调用read之前调用lseek,那 么两个线程最终会读取同一条记录。很显然这不是我们希望的。
为了解决这个问题,可以使用pread,使偏移量的设定和数据的读取 成为一个原子操作。
线程A 线程B pread(fd, buf1, 100, 300); pread(fd, buf2,
100, 700);
使用pread可以确保线程A读取偏移量为300的记录,而线程B读取偏 移量为700的记录。可以使用pwrite来解决并发线程对同一文件进行写操 作的问题。