线程同步(读写锁)
读写锁
读写锁(reader-writer lock)与互斥量类似,不过读写锁允许更高的并行性。
互斥量要么是锁住状态,要么就是不加锁状态,而且一次只有一个线程可以对其加锁。
读写锁可以有3种状态:
1、读模式下加锁状态
2、写模式下加锁状态
3、不加锁状态
读写锁特点:
- 一次只有一个线程可以占有写模式的读写锁,但是多个线程可以同时占有读模式的读写锁。
- 当读写锁是写加锁状态时,在这个锁被解锁之前,所有试图对这个锁加锁的线程都会被阻塞。(写状态下不可写也不可读)
- 当读写锁在读加锁状态时,所有以读模式对它进行加锁的线程都可以得到访问权,所有以写模式对此锁进行加锁的线程都会阻塞,直到所有的线程释放它们的读锁为止。(在读状态下不可写,但是可以被其他线程读)
- 当读写锁处于读模式锁住的状态,这时候一个线程试图以写模式来获取锁时,这个线程会进入阻塞,并且,其他线程随后的读模式的锁请求也同样会被阻塞。因为这样会避免读模式锁被长期占用,而处于阻塞等待的写模式锁的请求一直得不到满足。(不能插队)
- 读写锁非常适合于对数据结构读的次数远大于写的情况。
读写锁也叫共享互斥锁(shared-exclusive lock)。当读写锁是读模式锁住时,就可以说成是以共享模式锁住的。当它是写模式锁住的时候,就可以说成是以互斥模式锁住的。
读写锁使用之前必须初始化,在释放它们底层的内存之前必须销毁。
#include < pthread. h>
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
//两个函数的返回值:若成功,返回0;否则,返回错误编码
读写锁通过调用pthread_rwlock_init进行初始化。如果希望读写锁有默认的属性,可以传一个null 指针给attr。
Single UNIX Specification在XSI扩展中定义了PTHREAD_RWLOCK_INITIALIZER常量。 如果默认 属性就足够的话,可以用它对静态分配的读写锁进行初始化。
在释放读写锁占用的内存之前,需要调用pthread_rwlock_destroy做清理工作。
如果pthread_rwlock_init为读写锁分配了资源,pthread_rwlock_destroy将释放这些资源。
如果在调用pthread_rwlock_destroy之前就释放了读写锁占用的内存空间,那么分配给这个锁的资源就会丢失。(锁可能是调用malloc动态分配的,所以在释放分配的内存空间前,先调用pthread_rwlock_destroy将锁资源先释放)
#include <pthread. h>
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
//所有函数的返回值:若成功,返回0;若失败,否则,返回错误编码
各种实现可能会对共享模式下可获取读写锁的次数进行限制,所以需要检测pthread_rwlock_rdlock的返回值。而且从技术上来讲,在调用函数时应该总是检查错误返回,但是如果锁设计合理的话,就不需要检查它们。
Single UNIX Specification还定义了读写锁原语的条件版本:
#include <pthread. h>
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
//两个函数的返回值:若成功,返回0;否则,返回错误编码。
可以获取锁时,这两个函数返回0。否则,它们返回错误EBUSY。这两个函数可以用于遵守某种锁层次加锁但还是不能完全避免死锁的情况。
实例:
#include <pthread.h>
#include <stdlib.h>
//任务
struct job{
struct job *j_next;
struct job *j_prev;
pthread_t j_id; //标记哪一个线程处理这个任务
/*... more stuff here ...*/
};
//任务队列
struct queue{
struct job *q_head;
struct job *q_tail;
pthread_rwlock_t qlock;
};
/**
初始化一个队列
*/
int queue_init(struct queue *qp){
int err;
qp->q_head = NULL;
qp->q_tail = NULL;
err = pthread_rwlock_init(&qp->qlock, NULL);//初始化读写锁
if (err != 0) {
return err;
}
/* ... continue initialization ...*/
return 0;
}
/**
在任务队列头部插入一个任务
*/
void job_insert(struct queue *qp, struct job *jp){
pthread_rwlock_wrlock(&qp->qlock);//写模式下加锁
jp->j_next = qp->q_head;
jp->j_prev = NULL;
if (qp->q_head != NULL) {
qp->q_head->j_prev = jp;//队列不为空时
}else{
qp->q_tail = jp; //队列为空时
}
qp->q_head = jp;
pthread_rwlock_unlock(&qp->qlock);//解锁
}
/**
在队列尾部追加一个任务
*/
void jop_append(struct queue *qp, struct job *jp){
pthread_rwlock_wrlock(&qp->qlock);//写模式下加锁
jp->j_next = NULL;
jp->j_prev = qp->q_tail;
if (qp->q_tail != NULL) {
qp->q_tail->j_next = jp;//队列不为空时
}else{
qp->q_head = jp;//队列为空时
}
qp->q_tail = jp;
pthread_rwlock_unlock(&qp->qlock);//解锁
}
/**
从队列中移除一个任务
*/
void job_remove(struct queue *qp, struct job *jp){
pthread_rwlock_wrlock(&qp->qlock);//写加锁
if (jp == qp->q_head) {//移除的是第一个任务
qp->q_head = jp->j_next;
if (qp->q_tail == jp) { //只有一个任务的队列
qp->q_tail = NULL;
}else{
jp->j_next->j_prev = jp->j_prev;
}
}else if (jp == qp->q_tail){//移除的是尾部的任务
qp->q_tail = jp->j_prev;
jp->j_prev->j_next = jp->j_next;
}else{
jp->j_prev->j_next = jp->j_next;
jp->j_next->j_prev = jp->j_prev;
}
pthread_rwlock_unlock(&qp->qlock);
}
/**
根据线程id查找一个分配给该线程的任务
*/
struct job *job_find(struct queue *qp, pthread_t id){
struct job *jp;
if (pthread_rwlock_rdlock(&qp->qlock) != 0) {//读方式加锁
return NULL;
}
for (jp = qp->q_head; jp != NULL; jp = jp->j_next) {
if (pthread_equal(jp->j_id, id)) {
break;
}
}
pthread_rwlock_unlock(&qp->qlock);//解锁
return jp;
}
这个例子中,凡是需要向队列中增加任务或者从队列中删除任务的时候,都采用了写模式来锁住队列的读写锁。不管什么时候搜索队列,都需要获取读模式下的锁,允许所有的工作线程并发地搜索队列。
这种线程搜索作业的频率远远高于增加或删除作业时,使用读写锁才能改善性能。
带有超时的读写锁
与互斥量一样,Single UNIX Specification提供了带有超时的读写锁加锁函数,使应用程序在获取 读写锁时避免陷入永久阻塞状态。这两个函数是pthread_rwlock_timedrdlock和pthread_rwlock_timedwrlock。
#include <pthread. h>
#include <time. h>
int pthread_rwlock_timedrdlock(pthread_rwlock_t *restrict rwlock, const struct timespec *restrict tsptr);
int pthread_rwlock_timedwrlock(pthread_rwlock_t *restrict rwlock, const struct timespec *restrict tsptr);
//两个函数返回值:若成功,返回0;否则,返回错误编码号
这两个函数的行为与它们“不计时的”版本类似。tsptr参数指向timespec结构,指定线程应该停止阻塞的时间。如果它们不能获取锁,那么超时到期时,这两个函数将返回ETIMEDOUT错误。
与pthread_mutex_timedlock函数类似,超时指定的是绝对时间,而不是相对时间。