Linux c编程:同步属性
就像线程具有属性一样,线程的同步对象(如互斥量、读写锁、条件变量、自旋锁和屏障)也有属性
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_mutexattr_init函数用默认的互斥量属性初始化pthread_mutexattr_t结构。值得注意的两个属性是进程共享属性和类型属性。POSIX.1中,进程共享属性是可选的,可以通过检查系统中是否定义了_POSIX_THREAD_PROCESS_SHARED符号来判断这个平台是否支持进程共享这个属性,也可以在运行时把_SC_THREAD_PROCESS_SHARED参数传给sysconf函数进行检查。虽然这个选项并不是遵循POSIX标准的操作系统必须提供的,但是Single UNIX Specification要求遵循XSI标准的操作系统支持这个选项。
在进程中,多个线程可以访问同一个同步对象,这是默认的行为。在这种情况下,进程共享互斥量属性需要设置为PTHREAD_PROCESS_PRIVATE。
存在这样一种机制:允许相互独立的多个进程把同一个内存区域映射到它们各自独立的地址空间中。就像多个线程访问共享数据一样,多个进程访问共享数据通常也需要同步。如果进程共享互斥量属性设置为PTHREAD_PROCESS_SHARED,从多个进程共享的内存区域中分配的互斥量就可以用于这些进程的同步。
可以使用pthread_mutexattr_getpshared函数查询pthread_mutexattr_t结构,得到它的进程共享属性;可以用pthread_mutexattr_setpshared函数修改进程共享属性。
#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 );
返回值:若成功则返回0,否则返回错误编号
进程共享互斥量属性设为PTHREAD_PROCESS_PRIVATE时,允许pthread线程库提供更加有效的互斥量实现,这在多线程应用程序中是默认的情况。在多个进程共享多个互斥量的情况下,pthread线程库可以限制开销较大的互斥量实现。
类型互斥量属性控制着互斥量的特性。POSIX.1定义了四种类型。PTHREAD_MUTEX_NORMAL类型是标准的互斥量类型,并不做任何特殊的错误检查或死锁检测。PTHREAD_MUTEX_ERRORCHECK互斥量类型提供错误检查。
PTHREAD_MUTEX_RECURSIVE互斥量类型允许同一线程在互斥量解锁之前对该互斥量进行多次加锁。用一个递归互斥量维护锁的计数。在解锁的次数和加锁的次数不相同的情况下不会释放锁。所以如果对一个递归互斥量加锁两次,然后对它解锁一次,这个互斥量依然处于加锁状态,在对它再次解锁以前不能释放该锁。
最后,PTHREAD_MUTEX_DEFAULT类型可以用于请求默认语义。操作系统在实现它的时候可以把这种类型自由地映射到其他类型。例如,在Linux中,这种类型映射为普通的互斥量类型。
四种类型的行为如下表所示。“不占用时解锁”这一栏指的是一个线程对被另一个线程加锁的互斥量进行解锁的情况;“在已解锁时解锁”这一栏指的是当一个线程对已经解锁的互斥量进行解锁时将会发生的情况,这通常是编码错误所致。
可以用pthread_mutexattr_gettype函数得到互斥量类型属性,用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 );
两者的返回值都是:若成功则返回0,否则返回错误编号
互斥量可用于保护与条件变量关联的条件。在阻塞线程之前,pthread_cond_wait和pthread_cond_timedwait函数释放与条件相关的互斥量,这就允许其他线程获取互斥量、改变条件、释放互斥量并向条件变量发送信号。既然改变条件时必须占有互斥量,所以使用递归互斥量并不是好的办法。如果递归互斥量被多次加锁,然后用在调用pthread_cond_wait函数中,那么条件永远都不会得到满足,因为pthread_cond_wait所作的解锁操作并不能释放互斥量。
如果需要把现有的单线程接口放到多线程环境中,递归互斥量是非常有用的,但由于程序兼容性的限制,不能对函数接口进行修改。然而由于递归锁的使用需要一定技巧,它只应在没有其他可行方案的情况下使用。
使用递归锁的情况:
main()
{
func1(x);
......
func2(x);
}
func1()
{
pthread_mutex_lock(x->lock);
......
func2(x);
......
pthread_mutex_unlock(x->lock);
}
func2()
{
pthread_mutex_lock(x->lock);
......
pthread_mutex_unlock(x->lock);
}
上面的代码解释了递归锁看似解决并发问题的情况。假设func1和func2是函数库中现有的函数,其接口不能改变,因为存在调用这两个接口的应用程序,而且应用程序不能改动。
为了保持接口跟原来相同,可以把互斥量嵌入到数据结构中,把这个数据结构的地址(x)作为参数传入。这种方案只有在为该数据结构提供了分配函数时才可行,所以应用并不知道数据结构的大小(假设在其中增加互斥量后必须扩大该数据结构的大小)。
如果在最初定义数据结构时,预留了足够的可填充字段,允许把一些填充字段替换成互斥量,那么这种方法也是可行的。不过,大多数程序员并不善于预测未来,所以这不是普遍可行的经验。
如果func1和func2函数都必须操作这个结构,而且可能会有多个线程同时访问该数据结构,那么func1和func2必须在操作数据以前对互斥量加锁。当func1必须调用func2时,如果互斥量不是递归类型,那么就会出现死锁。如果能在调用func2之前释放互斥量,在func2返回后重新获取互斥量,那么就可以避免使用递归互斥量,但这也给其他的线程提供了机会,其他的线程可能在func1执行期间得到互斥量的控制权,修改这个数据结构。
避免使用递归锁的情况
main()
{
func1(x);
......
func2(x);
}
func1()
{
pthread_mutex_lock(x->lock);
......
func2_locked(x);
......
pthread_mutex_unlock(x->lock);
}
func2()
{
pthread_mutex_lock(x->lock);
func2_locked(x);
pthread_mutex_unlock(x->lock);
}
上面的代码显示了这种情况下使用递归互斥量的另一种替代方法。通过提供func2函数的私有版本(称之为func2_locked函数),可以保持func1和func2函数接口不变,并且避免使用递归互斥量。要调用func2_locked函数,必须占有嵌入到数据结构中的互斥量,这个数据结构的地址是作为参数传入的。func2_locked的函数体包含func2的副本(原来的、没有加入互斥量的func2),func2现在只是用以获取互斥量,调用func2_locked,最后释放互斥量。
如果并不一定要保持库函数接口不变,就可以在每个函数中另外再加一个参数,以表明这个结构是否被调用者锁定。但是,如果可能的话,保持接口不变通常是更好的选择,这样可以避免实现过程中人为加入的东西对原有接口产生不良影响。
提供函数的加锁版本和不加锁版本,这样的策略在简单的情况下通常是可行的。在比较复杂的情况下,例如库需要调用库以外的函数,而且可能会再次回调库中的函数时,就需要依赖递归锁。
下面解释了有必要使用递归互斥量的另一种情况。这里,有一个“超时”(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 */
#define USECTONSEC 1000 /* microseconds to nanoseconds */
void *
timeout_helper(void *arg)
{
struct to_info *tip;
tip = (struct to_info *)arg;
nanosleep(&tip->to_wait, NULL);
(*tip->to_fn)(tip->to_arg);
return(0);
}
void
timeout(const struct timespec *when, void (*func)(void *), void *arg)
{
struct timespec now;
struct timeval tv;
struct to_info *tip;
int err;
gettimeofday(&tv, NULL);
now.tv_sec = tv.tv_sec;
now.tv_nsec = tv.tv_usec * USECTONSEC;
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;
}
}
/*
* 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, "can't set recursive type");
if((err == pthread_mutex_init(&mutex, &attr)) != 0)
err_exit(err, "can't create recursive mutex");
/* ... */
pthread_mutex_lock(&mutex);
/* ... */
if(condition)
{
/* calculate target time "when" */
timeout(&when, retry, (void *)arg);
}
/* ... */
pthread_mutex_unlock(&mutex);
/* ... */
exit(0);
}
如果不能创建线程,或者安排函数运行的时间已过,问题就出现了。在这种情况下,要从当前环境中调用之前请求运行的函数,因为函数要获取的锁和现在占有的锁是同一个,除非该锁是递归的,否则就会出现死锁。
这里使用程序清单12-1中的makethread函数以分离状态创建线程。希望函数在未来的某个时间运行,而且不希望一直等待线程结束。
可以调用sleep等待超时到达,但它提供的时间粒度是秒级的,如果希望等待的时间不是整数秒,需要用nanosleep(2)函数,它提供了类似的功能。
timeout的调用者需要占有互斥量来检查条件,并且把retry函数安排为原子操作。retry函数试图对同一个互斥量进行加锁,因此,除非互斥量是递归的,否则如果timeout函数直接调用retry就会导致死锁。
读写锁属性
读写锁与互斥量类似,也具有属性。用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 * restrict attr, int *restrict pshared);
int pthread_rwlockattr_setpshared(pthread_rwlockattr_t *attr, int pshared);
两者的返回值都是:若成功则返回0,否则返回错误编号
条件变量属性
条件变量也有属性。与互斥量和读写锁类似,有一对函数用于初始化和回收条件变量属性。
#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_getpshared(const pthread_condattr_t *restrict attr, int *restrict pshared);
int pthread_condattr_setpshared(pthread_condattr_t *attr, int pshared);
两者的返回值都是:若成功则返回0,否则返回错误编号