分类: 系统运维
2012-03-31 23:06:30
正各线程有属性一样,它们的同步对象也有。在本节,我们讨论互斥体、读写锁和条件变量的属性。
互斥体属性
我们使用pthread_mutexattr_init来初始化一个phread_mutexattr_t结构体,和pthread_mutexattr_destroy来销毁它。
pthread_mutexattr_init 函数将用默认互斥体属性初始化pthread_mutexattr_t结构体。两个感兴趣的属性是进程共享(process-shared)属性和类型 (type)属性。在POSIX.1里,进程共享属性是可选的:你可以检查_POSIX_THREAD_PROCESS_SHARED符号是否定义来测试 一个平台是否支持它。你也可以调用参数为_SC_THREAD_PROCESS_SHARED给sysconf函数来在运行期测试。尽管这个可选项不需要 POSIX操作系统提供,然而SUS要求XSI操作系统必须支持这个选项。
在一个进程里,多线程可以访问同一个同步对象。这是默认行为,和11章看到的一样。在这种情况下,进程共享互斥体属性被设为PTHREAD_PROCESS_PRIVATE。
正 如我们将在14和15章看到的,存在机制允许独立的进程把相同的内存扩展映射到它们的独立地址空间里。访问多进程的共享数据通常需要同步,和访问多线程的 共享数据一样。如果进程共享互斥体属性被设为PTHREAD_PROCESS_SHARED,在多进程间共享的内存扩展里分配的一个互斥体可能用来这些进 程的同步。
我们可以使用pthread_mutexattr_getpshared函数来查询一个pthread_mutexattr_t结构体来得到进程共享属性。我们可以用pthread_mutexattr_setpshared函数改变进程共享属性。
进程共享互斥体属性允许pthread库提供更高效的实现,当属性被设为PTHREAD_PROCESS_PRIVATE时,这是多线程应用的默认情况。然后pthread库可以在多进程之间共享互斥体的情况下限制更昂贵的实现。
类型互斥体属性控制了互斥体的特性。POSIX.1定义了四种类型。PTHREAD_MUTEX_NORMAL类型是不作任何错误检查或死锁检测的标准互斥体。PTHREAD_MUTEX_ERRORCHECK互斥体类型提供了错误检查。
PTHREAD_MUTEX_RECUSIVE互斥体类型允许相同的线程锁住它多次而不必事先解锁。一个递归互斥体维护一个锁计数并直到它被解锁次数与加锁次数相同才会被释放。
最终,PTHREAD_MUTEX_DEFAULT类型可以用来请求默认语义。实现可以自由地把它映射到其它某个类型。例如,在Linux,这个类型被映射被映射到普通互斥体类型。
四种类型的行为被展示在下表。“当不被拥有时解锁”列表示一个线程解锁一个被一个不同线程锁住的互斥体。“当无锁时解锁”列表示当一个线程解锁一个已经解锁的互斥体会发生什么,它通常是一个代码错误。
互斥体类型 | 未解锁时重新加锁? | 当不被拥有时解锁? | 当无锁时解锁? |
---|---|---|---|
PTHREAD_MUTEX_NORMAL | 死锁 | 无定义 | 无定义 |
PTHREAD_MUTEX_ERRORCHECK | 返回错误 | 返回错误 | 返回错误 |
PTHREAD_MUTEX_RECURSIVE | 允许 | 返回错误 | 返回错误 |
PTHREAD_MUTEX_DEFAULT | 无定义 | 无定义 | 无定义 |
我们可以用pthread_mutexattr_gettype来得到互斥体类型属性,pthread_mutexattr_settype来改变互斥体属性。
回 想11.6节,一个互斥体被用来保护和一个条件变量相关的条件。在阻塞线程前,pthread_cond_wait和 pthread_cond_timedwait函数释放了条件相关的互斥体。这允许其它线程申请互斥体,改变条件,释放互斥体,并发信号给条件变量。因为 互斥体必须被握住来改变条件,所以使用一个递归互斥体不是好主意。如果一个递归互斥体被锁住多次并在pthread_cond_wait调用里使用,那么 条件永远不能被满足,因为pthread_cond_wait完成的解锁不会释放这个互斥体。
在你需要把已有的单线程接口适配到多线程环境里,但因为兼容性限制又不能改变函数接口时,递归互斥体很有用。尽管如此,使用递归锁会很复杂,它们只在没有其它可能的解决方案时使用。
下面的例子演示了一个递归互斥体可能似乎解决一个并发问题的情况:假定func1和func2是库里已有的函数,它们接受结构体的地址作为参数,设它为x。func1(x)调用func2(x)。它们的接口不能改变,因为存在应用调用它们,而应用不能被改变。
为了保持接口不变,我们在其地址被作为参数传递的数据结构里内嵌一个互斥体,设它为x->lock。这只在我们为这个结构体提供一个分配器函数时才有可能,所以应用不知道它的尺寸(假定我们在为它加入一个互斥体时必须增加它的尺寸)。
如果我们最初定义这个结构体时预留了一些我们现在可以添加一个互斥体时的空间,这也是有可能的。不幸的是,多数程序员没有预测未来的技能,所以这不是一个普遍的实践。
如 果func1和func2都必须操作这个结构体而它可能同时被多个线程访问,那么func1和func2必须在操作这个数据前锁住这个互斥体。如果 func1必须调用func2,那么如果互斥体类型不是递归的话我们会死锁,因为x->lock在func1里已经锁住,func2里再次尝试锁住 同一个互斥体。我们可以避免使用递归互斥体,如果我们在调用func2之前释放互斥体,并在func2返回后申请它,但是这打开了一个时间间隙,此时另一 个线程可能可以得到互斥体的控制并在func1运行到一半时改变这个数据结构。这可能是不可接受的,取决于互斥体意图提供的保护。
下面是这 种情况下递归互斥体的一个替代方式:我们不改变func1和func2的接口,也避免使用一个递归互斥体,通过提供func2的一个私有版本,称为 func2_locked。为了调用func2_locked,我们必须握住其地址作为参数的结构体内部的互斥体(x->lock)。 func2_locked包含func2函数体的复制,而fun2只是简单地申请互斥体,调用func2_locked,然后释放互斥体。而func1也 直接调用func2_locked,这样就不必多次申请同一个锁。
如果我们不要保持库函数接口,我们可以给每个函数加上第二个参数来指定这个结构体是否被调用者锁住。尽管如此,通常如果可以的话最多都保持接口不变,而不是用实现方式的产物来污染它。
在简单情况下,提供函数的锁版本和无锁版本的策略通常都可行。在更复杂的情况,比如当库需要调用库外面的一个函数,这个函数然后可能调回到这个库里,那么我们需要依赖于递归锁。
下面的代码演示了另一个需要递归锁的情况。这里,我们有一个“计时”函数,允许我们安排另一个函数在将来某时运行。假定线程是不昂贵的资源,我们可以为每个待定定的计时创建一个线程。线程一直等到时间到达,然后它调用我们请求的函数。
我们使用12.3节的makethread函数来创建分离状态的线程。我们将这个函数在将来运行,而不想等待线程的完成。
我们可以调用sleep来等待计时过期,但是那只给了我们秒的粒度。如果我们想等待一些不是整数秒的时间,那么我们需要使用nanosleep,它提供了相似的功能。
尽管nanosleep只被要求实现在SUS的实时扩展里,但是本文所有的平台都支持它。
timeout的调用者需要握住一个互斥体把检查条件和安排retry函数作为一个原子操作。retry函数将尝试锁住相同的互斥体。除非这个互斥体是递归的,否则如果timeout直接调用retry会有死锁发生。
读写锁属性
读写锁也有属性,和互斥体相似。我们使用pthread_rwlockattr_init来初始化一个pthread_rwlockattr_t结构体和pthread_rwlockattr_destroy来反初始化这个结构体。
为读写锁支持的唯一的属性是进程共享属性。它和互斥体的进程共享属性一样。正如互斥体进程共享属性,一对函数被提供来得到和设置读写锁的进程共享属性。
尽管POSIX只定义一个读写锁属性,实现可以定义补充的非标准的属性。
条件变量属性
条件变量也有属性。有一对函数用来初始化和反初始化它们,和互斥体与读写锁一样。
和其它同步原始对象一样,条件变量也支持进程共享属性。