12.4 同步属性
12.4.1 互斥锁属性
互斥锁属性使用结构pthread_mutexattr_t结构进行存储,在11章中,我们使用PTHREAD_MUTEX_INITIALIZER常量或者是调用函数pthread_mutex_init并传参NULL指针来指定互斥锁属性的方法实现使用默认属性初始化互斥锁。
#include <pthread.h>
int pthread_mutexattr_init(pthread_mutexattr_t *attr);
int pthread_mutexattr_destroy(pthread_mutex_attr_t *attr);
函数pthread_mutexattr_init函数用于初始化pthread_mutexattr_t结构为默认互斥锁属性,有如下三项感兴趣的属性:process-shared属性,robust属性,以及type属性。在POSIX.1中,process-shared属性是可选的,你可以测试系统平台上是否定义了宏_POSIX_THREAD_PROCESS_SHARED标志。你也可以在运行时候通过函数sysconf传参_SC_THREAD_PROCESS_SHARED来进行检查。虽然符合POSIX标准的实现并不要求实现这一属性,但是符合the Single Unix Specification标准的XSI选项的实现要求必须支持该属性。
在一个进程中,多线程可以访问相同的同步对象,这是我们在11章中看到的默认行为,在这种情况下,process-shared互斥锁属性将被设置未PTHREAD_PROCESS_PRIVATE.
我们将在第14,15章中看到,存在这样一种机制:允许独立的进程将相同的内存段映射到进程的独立地址空间内,多进程对于共享数据的访问通常需要进行同步,就像多线程访问共享数据一样。如果process-shared互斥锁属性被设置为PTHEAD_PROCESS_SHARED,那么这个互斥锁就可以被多个进程用于同步。
我们可以使用函数pthread_mutexattr_getshared函数来查询pthread_mutexattr_t结构的process-shared属性,我们也可以使用函数pthread_mutexattr_setpshared修改process-shared属性。
#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);
Both return : 0 if OK, error number on failure.
当process-shared互斥锁属性被设置为PTHREAD_PROCESS_PRIVATE的时候,允许pthread线程库提供更加高效的实现,这也是多线程应用程序默认的使用方式。pthread限制了更加复杂的实现仅仅用于互斥锁被多个进程共享的情况。
互斥锁的robust属性与多进程共享的互斥锁有关,其目的是解决如下情况下的互斥锁恢复问题:一个进程终止的时候仍然锁定了一个互斥锁,然后这个互斥锁将会一直保持锁定状态,恢复将比较困难,阻塞在同一互斥锁上的其他进程中的线程将会永久被阻塞。
#include <pthread.h>
int pthread_mutexattr_getrobust(const pthread_mutex_attr_t *restrict attr, int *restrict robust);
int pthread_mutexattr_setrobust(pthread_mutexattr_t *attr, int robust);
Both return: 0 if OK, error number on failure.
对于robust的值存在两种可能。其默认值是PTHREAD_MUTEX_STALLED,该值意味这在进程锁定互斥锁的状态下终止,并不会采取什么特别的操作进行处理,在这种情况下,互斥锁可能导致未定义行为。等待该互斥锁可用的线程将会一直等待.另外一个可能的取值是PTHREAD_MUTEX_ROBUST.当出现进程锁定互斥锁终止的情况是,该值将会造成阻塞在函数pthread_mutex_lock中的线程能够成功获取到互斥锁,但是pthead_mutex_lock的返回值并不是0,而是EOWNERDEAD.应用程序可以使用判断返回值,××并在出现这样的情况的时候尝试恢复互斥锁的状态××。注意EOWNERDEAD错误并不是一个真正的错误,因为调用函数将成功获取到互斥锁。
使用robust mutexs会改变我们使用函数pthread_mutex_lock的方法:因为我们现在必须检查三种返回值而不再是两种:不需要恢复的成功,需要恢复的成功以及失败。而如果我们不使用robust mutexs,那么我们可以继续只是检查成功和失败。
在本书中介绍的四个平台上,仅仅只有Linux 3.2.0 支持robust pthread mutexes.Solaris 10仅仅在其Solaris线程库内支持robust mutexes,但是Solaris 11支持robust mutexs.
如果互斥锁的一致性状态没有被恢复,那么互斥锁将在解除对其锁定以后变得永远不可用。为了防止出现这一问题,线程可以在解除互斥锁锁定之前调用函数pthread_mutex_consistent来恢复与互斥锁相关联的状态的一致性。
#include <pthread.h>
int pthread_mutex_consistent(pthread_mutex_t *mutex);
Returns: 0 if OK, error number on failure.
如果线程在解除互斥锁锁定状态之前没有使用函数pthread_mutex_consistent恢复互斥锁一致性,那么其他尝试获取互斥锁的线程将会看到一个错误返回值ENOTRECOVERABLE.如果出现这样的情况,互斥锁将不再可用。如果首先调用函数pthread_mutex_consistent,互斥锁状态将会恢复正常,因此它可以继续正常使用。
互斥锁属性的type属性控制了互斥锁锁定特点,POSIX.1定义了四种类型:
类型 | 描述 |
---|---|
PTHREAD_MUTEX_NORMAL | 标准互斥锁类型,并不会执行任何特殊错误检查以及死锁检测 |
PTHREAD_MUTEX_ERRORCHECK | 提供错误检测的互斥锁 |
PTHREAD_MUTEX_RECURSIVE | 一种允许同一线程在解除锁定之前多次锁定的互斥锁类型,递归互斥锁会保存一个锁计数,在解锁次数达到与锁定次数相同之前并不会释放锁。也就是说,如果你锁定互斥锁两次,然后解锁一次,互次锁仍然保持锁定状态,直到再一次解锁互斥锁 |
PTHREAD_MUTEX_DEFAULT | 默认互斥锁类型,实现可以自由定义该类型未其他任意类型的互斥锁类型,比如说,Linux3.2.0将该类型映射为正常互斥锁类型,同时FreeBSD 8.0将其映射为错误检测类型 |
上述四种类型的互斥锁的表现特点将总结如表12.5所示
互斥锁类型 | 解锁前再次尝试锁定 | 解锁其他线程拥有的互斥锁 | 解锁未锁定状态的互斥锁 |
---|---|---|---|
PTHREAD_MUTEX_NORMAL | 死锁 | 未定义 | 未定义 |
PTHREAD_MUTEX_ERRORCHECK | 返回错误 | 返回错误 | 返回错误 |
PTHREAD_MUTEX_RECURSIVE | 允许 | 未定义 | 未定义 |
PTHREAD_MUTEX_DEFAULT | 未定义 | 未定义 | 未定义 |
图12.5 互斥锁类型的表现
我们可以使用函数pthread_mutexattr_gettype来获取互斥锁的type属性,为了修改属性,我们可以使用函数pthread_mutexattr_settype.
#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)
在11.6.6节中我们有讲到使用互斥锁保护与条件变量相关联的条件。在锁定线程之前,函数pthread_cond_wait或者是pthread_cond_timedwait将会释放与条件变量相关联的互斥锁。这允许其他线程获取互斥锁,从而修改条件变量,释放互斥锁,最后发出相应信号。由于修改条件的时候互斥锁必须处于锁定状态,因此使用递归互斥锁并不是一个好注意,如果递归互斥锁被多次锁定,并被用于函数pthread_cond_wait,那么条件变量将永远不会满足,因为函数pthread_cond_wait函数执行的解锁操作并没有真正释放掉互斥锁。
递归互斥锁在你需要在多线程环境中使用单线程接口的情况下比较有用,然而,使用递归互斥锁仍然比较危险,除非没有其他的解决方法,否则不建议使用递归互斥锁。
Example
图12.6阐述了这样一种情况:递归互斥锁可以解决并行问题。假设函数func1以及func2是已经存在的库函数,其接口是不可修改的,因为存在应用程序调用这两个函数,并且这些应用是不可修改的。
为了保持接口一致,我们嵌入一个互斥锁到地址为(x)并且作为参数传递的数据结构中,如果我们提供了一个结构的分配函数那么这就是可能实现的,因此应用程序并不知道其大小(假设我们在增加互斥锁到结构中的时候会增加其尺寸)。
如果我们原来定义的结构中预留了区域允许我们保存互斥锁。上述实现也是可能的,不幸的是,许多程序员在未来的预测方面并没有什么技巧,所以这并不是一个普遍的例子。
如果函数fun1与fun2都必须操作这个结构,并且可能存在多个线程同时对同一结构进行访问,那么函数fun1与fun2在对结构进行操作之前必须要先进行互斥锁的锁定,如果互斥锁fun1必须调用函数fun2,那么如果互斥锁不是递归的就会出现死锁的情况。当然,如果我们可以在调用函数fun2之前先释放掉互斥锁,并在fun2返回之后再次获取互斥锁,那么就可以避免递归互斥锁的使用,但是这样做的话会在fun2调用前后开启一个时间窗口,在这个时间窗口内其他线程可能会成功获取互斥锁,然后在func1中间对数据结构进行修改,这可能是不可接受的。
图12.7阐述了另外一个使用递归互斥锁的例子,我们可以不修改func1与func2的接口,通过提供一个私有版本的fun2函数来避免递归互斥锁的使用,并称之为func2_locked,我们必须将互斥锁嵌入保存到我们传递地址参数的数据结构中,fun2_locked函数体包含了fun2函数体的复制,而fun2现在只是简单地获取互斥锁,调用函数fun2_locked,然后释放互斥锁。
当然,如果我们不需要保持库函数接口不变,我们就可以增加一个参数来表示数据结构是否被锁定了,如果可以的话,多数情况下保持结构不变是比较好的。
提供锁定版本与未锁定版本函数的方法在简单情况下通常是可以的,但是在更加复杂的情况下,比如说当库函数需要调用函数库外的函数,而被调用的函数库外的函数然后会回调函数库内的函数,我们就需要使用递归锁了。
Example
图12.8中的程序阐述了需要使用递归互斥锁的另一个例子,在程序中有一个”timeout”函数,该函数的功能是允许我们在未来某一时刻运行另一个函数。如果线程资源是不昂贵的,那么我们就可以为每一个挂起的timeout创建一个线程,这些线程处于等待状态直到定时结束,然后调用请求的函数。
但是如果我们不能创建线程或者是调度的时间已经过了,那么问题就出现了,这这种情况下,我们简单地调用请求的函数。因为函数需要我们当前保持锁定的锁,死锁就会出现,除非锁是递归的。
#include "apue.h"
#include <pthread.h>
#include <time.h>
#include <sys/time.h>
extern int makethread(void *(*)(void *), void *);
struct to_info
{
void (*to_fn)(void *); /* function */
void *to_arg; /* argument */
struct timespec to_wait; /* time to wait */
};
#define SECTONSEC 1000000000 /* seconds to nanoseconds */
#if !defined(CLOCK_REALTIME) || defined(BSD)
#define clock_nanosleep(ID, FL, REQ, REM) nanosleep((REQ), (REM))
#endif
#ifndef CLOCK_REALTIME
#define CLOCK_REALTIME 0
#define USECTONSEC 1000 /* microseconds to nanoseconds */
void clock_gettime(int id, struct timespec *tsp)
{
struct timeval tv;
gettimeofday(&tv, NULL);
tsp->tv_sec = tv.tv_sec;
tsp->tv_nsec = tv.tv_usec * USECTONSEC;
}
#endif
void *timeout_helper(void *arg)
{
struct to_info *tip;
tip = (struct to_info *)arg;
clock_nanosleep(CLOCK_REALTIME, 0, &tip->to_wait, NULL);
(*tip->to_fn)(tip->to_arg);
free(arg);
return 0;
}
void timeout(const struct timespec *when, void (*func)(void *), void *arg)
{
struct timespec now;
struct to_info *tip;
int err;
clock_gettime(CLOCK_REALTIME, &now);
if((when->tv_sec > now.tv_sec) || (when->tv_sec == now.tv_sec && when->tv_nsec > now.tv_nsec))
{
tip = malloc(sizeof(struct to_info));
if(tip != NULL)
{
tip->to_fn = func;
tip->to_arg = arg;
tip->to_wait.tv_sec = when->tv_sec - now.tv_sec;
if(when->tv_nsec >= now.tv_nsec)
{
tip->to_wait.tv_nsec = when->tv_nsec - now.tv_nsec;
}
else
{
tip->to_wait.tv_sec--;
tip->to_wait.tv_nsec = SECTONSEC - now.tv_nsec + when->tv_nsec;
}
err = makethread(timeout_helper, (void *)tip);
if(err == 0)
{
return;
}
else
{
free(tip);
}
}
}
/* We get here if (a) when <= now, or (b) malloc fails. or (c) we can't make a thread, so we just call the function now. */
(*func)(arg);
}
pthread_mutexattr_t attr;
pthread_mutex_t mutex;
void retry(void *arg)
{
pthread_mutex_lock(&mutex);
/* perform retry steps ... */
pthread_mutex_unlock(&mutex);
}
int main(void)
{
int err, condition, arg;
struct timespec when;
if((err = pthread_mutexattr_init(&attr)) != 0)
{
err_exit(err, "pthread_mutexattr_init failed");
}
if((err = pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE)) != 0)
{
err_exit(err, "cann;t set recursive type");
}
if((err = pthread_mutex_init(&mutex, &attr)) != 0)
{
err_exit(err, "cann't create recursive mutex");
}
/* continue processing ... */
pthread_mutex_lock(&mutex);
/* Check the condition under the protection of a lock to make the check and the call to timeout atomic. */
if(condition)
{
/* Calculate the absolute time when we want to retry */
clock_gettime(CLOCK_REALTIME, &when);
when.tv_sec += 10; /* 10 seconds from now */
timeout(&when, retry, (void *)((unsigned long)arg));
}
pthread_mutex_unlock(&mutex);
/* continue processing */
exit(0);
};
图12.8 使用递归锁
我们使用函数makethread(图12.4)来创建一个处于分离状态的程序,因为函数func将在未来运行,因此我们并不想要等待线程完成。
我们可以使用函数sleep来等待设置的时间到达,但是sleep只能达到秒级时间精度,如果我们想要等待的时间并不是整数秒钟,那么我们需要使用函数nanosleep或者是clock_nanosleep,这两个函数都可以实现更高精度的睡眠时间。
在没有定义CLOCK_REALTIME的系统上,我们根据nanosleep函数定义了函数clock_nanosleep。FreeBSD8.0定义了这一标志,支持clock_gettime以及clock_settime,但是不支持clock_nanosleep,目前仅仅只有Linux3.2.0以及Solaris 10支持函数clock_nanosleep.
此外,在没有定义CLOCK_REALTIME的系统上,我们提供了我们自己的clock_gettime实现,该实现调用了函数gettimeofday来将毫秒转换成纳秒。
函数timeout的调用这需要使用一个互斥锁来保证检查condition变量的过程与retry函数的调度是原子操作,除非互斥锁是递归的,否则,一旦timeout函数直接调用函数retry,死锁问题就会立即出现。
12.4.2 读写锁属性
#include <pthread.h>
int pthread_rwlockattr_init(pthread_rwlockattr_t *attr);
int pthread_rwlockattr_destroy(pthread_rwlockattr_t *attr);
Both return: 0 if OK, error number on failure.
读写锁唯一支持的属性是process-shared属性,等同于互斥锁的process-shared属性。
#include <pthread.h>
int pthread_rwlockattr_getshared(const pthread_rwlockattr_t *restrict attr, int *restrict pshared);
int pthread_rwlockattr_setpshared(pthread_rwlockattr_t *attr, int pshared);
Both return: 0 if OK, error number on failure.
虽然POSIX仅仅定义了一个读写锁属性,但是实现可以自由定义额外的非标准属性。
12.4.3 条件变量属性
The Single Unix Specification目前为条件变量定义了两个属性:process-shared以及clock属性
#include <pthread.h>
int pthread_condattr_init(pthread_condattr_t *attr);
int pthread_condattr_destroy(pthread_condattr_t *attr);
Both return: 0 if OK, error number on failure.
process-shared属性与其他同步属性是一样的,控制了条件变量的使用范围是单进程内的多线程还是多进程内。
#include <pthread.h>
int pthread_condattr_getshared(const pthread_condattr_t *restrict attr, int *restrict pshared);
int pthread_condattr_setshared(pthread_condattr_t *attr, int pshared);
Both return: 0 if OK, error number on failure.
clock属性控制了在使用函数pthread_cond_timedwait的时候使用哪一个时钟来计算超时参数tsptr.其合法取值是表6.8中列出的时钟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);
Both return: 0 if OK, error numbet on failure.
奇怪的是,The Single Unix Specification并没有为有超时等待的其他同步对象定义clock属性。
12.4.4 Barrier属性
#include<pthread.h>
int pthread_barrierattr_init(pthread_barrierattr_t *attr);
int pthread_barrierattr_destroy(pthread_barrierattr_t *attr);
Both return: 0 of OK, error number on failure.
barrier属性当前仅仅由process-shaerd属性;
#include <pthread.h>
int pthread_barrierattr_getshared(const pthread_barrierattr_t * restrict attr, int *restrict pshared);
int pthread_barrierattr_setshared(pthread_barrierattr_t *attr, int pshared);
Both return: 0 if OK, error number on failure.
process-shared的属性值也可以设置成PTHREAD_PROCESS_SHARED(可以被多进程中的线程访问)或者是PTHREAD_PROCESS_PRIVATE(仅仅初始化barrier的进程可以访问);