一、读写锁的概念

  • 读写锁与互斥量类似。但是互斥量要么是锁住状态,要么就是不加锁状态,而且一次只有一个线程可以对其加锁
  • 不过读写锁允许更高的并行性,而且有更多的状态

读写锁可以有3种状态:

  • ①读模式下加锁
  • ②写模式下加锁
  • ③不加锁状态

一次只有一个线程可以占有写模式的读写锁,但是多个线程可以同时占有读模式的读写锁

二、读写锁的使用

  • 当读写锁是​写加锁​状态时:在这个锁被解锁之前,所有试图​对这个锁加锁​的线程都会​阻塞(不论是读还是写)
  • 当读写锁是​读加锁​状态时:所有试图以​读模式​对它进行加锁的线程都​可以得到访问权​。但是任何希望以​写模式​对此锁​进行加锁​的线程都会​阻塞​,直到所有的线程释放它们的读锁为止

虽然各操作系统对读写锁的实现各不同,但当读写锁处于读模式锁住的状态,而这时有一个线程试图以写模式获取锁时,读写锁通常会阻塞随后的读模式锁请求。这样可以避免读模式锁长期占用,而等待的写模式锁请求一直得不到满足


读写锁的应用:读写锁非常适合于对数据结构读的次数大于写的情况

  • 当读写锁在写模式下时:​它所保护的数据结构就可以被安全的修改,因为一次只有一个线程可以在写模式下拥有这个锁
  • 当读写锁在读模式下时:​只要线程先获取了读模式下的读写锁,该锁所保护的数据结构就可以被多个获得读模式锁的线程读取


三、读写锁别名

读写锁也称为共享互斥锁

  • 当读写锁是读模式锁住时,就可以说成是以共享模式锁住的
  • 当它是写模式锁住时,就可以说成是以互斥模式锁住的

四、读写锁变量(pthread_rwlock_t)

  • 读写锁数据类型:pthread_rwlock_t

五、读写锁的初始化与释放


①静态初始化

  • 直接把pthread_rwlock_t互斥变量设置为常量​PTHREAD_RWLOCK_INITIALIZER
  • 静态初始化读写锁变量​只能拥有默认的读写锁属性​,不能设置其他读写锁属性(读写锁属性见文章:
  • 例如:

pthread_rwlock_t rwlock;
rwlock=PTHREAD_RWLOCK_INITIALIZER;

//或者
pthread_rwlock_t *rwlock=(pthread_rwlock_t *)malloc(sizeof(pthread_rwlock_t));
*rwlock=PTHREAD_MUTEX_INITIALIZER;



②动态初始化

  • 静态初始化读写锁变量只能拥有默认读写锁属性,我们可以通过pthread_rwlock_init函数来动态初始化读写锁,并且可以在初始化时选择设置读写锁的属性

#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:

  • 功能:对读写锁变量进行初始化
  • 参数:
  • 参数1:​初始化的读写锁
  • 参数2:​读写锁初始化时的属性。如果用默认属性,此处填NULL

pthread_rwlock_destory:

  • 功能:​读写锁变量的反初始化,释放销毁
  • 参数:​读写锁变量
  • 如果在pthread_rwlock_destory之前就释放了读写锁占用的内存空间,那么分配给这个锁的资源就会丢失
  • 备注(重点):​此函数只是反初始化读写锁变量,并没有释放内存空间,如果读写锁变量是通过malloc等函数申请的,那么需要在free掉读写锁变量之前调用pthread_rwlock_destory函数

例如:

pthread_rwlock_t rwlock;
pthread_rwlock_init(&rwlock,NULL);

/*do something*/

pthread_rwlock_destory(&rwlock);
pthread_rwlock_t* rwlock=(pthread_mutex_t*)malloc(sizeof(pthread_mutex_t));
pthread_rwlock_init(rwlock,NULL);

/*do something*/

pthread_rwlock_destory(rwlock);
free(rwlock);


五、加锁与解锁函数

#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:​在读模式下锁定读写锁
  • pthread_rwlock_wrlock:​在写模式下锁定读写锁
  • pthread_rwlock_unlock:​不管以何种方式锁住读写锁,都可以用这个函数解锁

返回值的注意事项:

  • 各种实现可能会对共享模式下可获取的读写锁的次数进行限制,所以需要检查pthread_rwlock_rdlock的返回值
  • 即使pthread_rwlock_wrlock和pthread_rwlock_unlock有错误返回,而且从技术上来讲,在调用函数时应该总会检查错误返回,但是如果锁设计合理的话,就不需要检查它们。错误返回值的定义只是针对不正确使用读写锁的情况(如未经初始化的锁),或者试图获取已拥有的锁从而可能产生死锁的情况
  • 但是还需要注意,有些特定的实现可能会定义另外的错误返回




#include <pthread.h>
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);

//返回值:成功返回0;否则返回错误编号

这两个函数类似于pthread_mutex_trylock函数:

  • pthread_rwlock_tryrdlock:​尝试获得读模式的读写锁,如果可以获取返回0,不可以获取出错返回EBUSY
  • pthread_rwlock_trywrlock:​尝试获得写模式的读写锁,如果可以获取返回0,不可以获取出错返回EBUSY

这两个函数可以用于我们前面讨论的遵守某种锁层次但还不能完全避免死锁的情况


六、案例

  • 下面的程序解释了读写锁的使用。作业请求队列由单个读写锁保护。这个例子给出了下图所示的一种可能的实现,多个工作线程获取单个主线程分配给它们的作业

APUE编程:60---线程处理(读写锁:pthread_rwlock_t)_读写锁

#include <stdlib.h>
#include <pthread.h>

struct job {
struct job *j_next;
struct job *j_prev;
pthread_t j_id; /* tells which thread handles this job */
/* ... more stuff here ... */
};

struct queue {
struct job *q_head;
struct job *q_tail;
pthread_rwlock_t q_lock;
};

//Initialize a queue.
int queue_init(struct queue *qp)
{
int err;

qp->q_head = NULL;
qp->q_tail = NULL;
err = pthread_rwlock_init(&qp->q_lock, NULL);
if (err != 0)
return(err);
/* ... continue initialization ... */
return(0);
}

//Insert a job at the head of the queue.
void job_insert(struct queue *qp, struct job *jp)
{
pthread_rwlock_wrlock(&qp->q_lock);
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; /* list was empty */
qp->q_head = jp;
pthread_rwlock_unlock(&qp->q_lock);
}

//Append a job on the tail of the queue.
void job_append(struct queue *qp, struct job *jp)
{
pthread_rwlock_wrlock(&qp->q_lock);
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; /* list was empty */
qp->q_tail = jp;
pthread_rwlock_unlock(&qp->q_lock);
}

//Remove the given job from a queue.
void job_remove(struct queue *qp, struct job *jp)
{
pthread_rwlock_wrlock(&qp->q_lock);
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->q_lock);
}

//Find a job for the given thread ID.
struct job * job_find(struct queue *qp, pthread_t id)
{
struct job *jp;

if (pthread_rwlock_rdlock(&qp->q_lock) != 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->q_lock);
return(jp);
}

APUE编程:60---线程处理(读写锁:pthread_rwlock_t)_加锁_02

七、带有超时的读写锁

#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;否则返回错误编号
  • 与互斥量一样,Single UNIX Specification提供了带有超时的读写锁加锁函数,使应用程序在获取读写锁时避免陷入永久阻塞状态
  • 如果时间到期超时时,我们不能获取锁,两个函数返回ETIMEOUT错误
  • 注意:​这个时间值是一个绝对数而不是相对数。例如,假设愿意等待3分钟,那么不是把3分钟转换为timespec结构体,而是需要把当前时间加上3分钟再转换成timespec结构(可以查看互斥量的演示案例: