linux读写锁
读写锁与互斥量
读写锁(reader-writer lock)和互斥量(mutex)类似,不过读写锁拥有更高的并行性。
互斥,顾名思义,同一时刻只有一个线程能够对其加锁,所以互斥量在同一时刻只可能处于如下两种状态之一:
- 锁住状态
- 不加锁状态
读写锁则不同,与互斥量相比,它区分出了读锁,和写锁,所以读写锁可以有三种状态:
- 读模式下加锁状态,简称读加锁状态
- 写模式下加锁状态,简称写加锁状态
- 不加锁状态
一次只有一个线程可以占用写模式的读写锁,但是多个线程可以同时占有读模式的读写锁。
读写锁介绍
当读写锁是写加锁状态时,在这个锁被解锁之前,所有试图对这个锁加锁的线程都会被阻塞。
当读写锁是读加锁状态时,所有试图以读模式对它进行加锁的线程都可以得到访问权,但是任何试图以写模式对它进行加锁的线程都会被阻塞,直到所有的线程释放它们的锁为止。
所以读写锁也叫做共享互斥锁(shared-exclusive lock),当读写锁是读加锁状态时,就可以说成是以共享模式(shared)锁住的,当读写锁是写加锁状态时,也可以说成是互斥模式(exclusive)锁住的。
虽然各种操作系统对读写锁的实现各不相同,但是当读写锁处于读加锁状态,而这时有一个线程试图以
写模式获取读写锁的时候,读写锁通常会阻塞后面的读模式加锁请求,这样可以避免读模式长期占用,而等待的写模式锁请求一直得不到满足。
读写锁适用场景
读写锁非常适用于对数据结构读的次数远大于写的次数,当读写锁处于写加锁状态时,它保护的数据结构就能够被安全地修改,因为一次只有一个线程能够在写模式下拥有这个锁。
当读写锁处于读加锁状态时,只要线程先获得了读模式锁,该锁保护的数据结构就可以被多个获得读模式锁的线程读取。
获取读写锁的系统接口
与互斥量相比,读写锁在使用之前必须初始化,在使用完后必须销毁。
#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进行初始化。
读写锁通过调用pthread_rwlock_destroy做清理工作,如果
pthread_rwlock_init为读写锁分配了资源,pthread_rwlock_destroy将释放这些资源。
通过调用pthread_rwlock_rdlock获得读模式锁,通过调用pthread_rwlock_wrlock获得写模式锁,不管以何种方式锁住读写锁,都可以调用pthread_rwlock_unlock进行解锁。
#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函数的官方文档的话,可能会看到下面的一段话:
The maximum number of simultaneous read locks that an implementation guarantees can be applied to a read-write lock shall be implementation-defined. The pthread_rwlock_rdlock() function may fail if this maximum would be exceeded.
这段话的意思是linux系统的各种实现可能会对共享模式下可获取的读写锁的次数进行限制,使用者可以通过检查pthread_rwlock_rdlock函数的返回值来确定错误原因。
应用实例
下面的程序介绍了读写锁的使用。
主线程将工作任务放到一个工作队列中,用线程ID控制每个工作线程处理那些作业,如下图所示,主线程将新的工作任务放到工作队列中,由三个工作线程组成的线程池从工作队列中移出工作任务。主线程会在每个待处理的工作任务的结构中放置处理该任务的线程ID,每个工作线程只能够移出标有自己线程ID的工作任务。
作业请求队列由单个读写锁保护,多个工作线程获取单个主线程分配给它们的作业。
#include <stdlib.h>
#include <pthread.h>
//job结构体定义
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);
}
在这个例子中,凡是向队列中增加任务或者从队列中删除任务的时候,都采用写模式锁来锁住队列的读写锁;不管何时搜索队列,都需要获取读模式下的读写锁,允许所有的工作线程并发地搜索队列。在这种情况下,只有当线程搜索任务的频率远远高于增加或删除任务时,使用读写锁才可能改善性能。