读写锁(reader-writer lock)
读写锁(reader-writer lock)与互斥量类似,不过读写锁允许更高的并行性。互斥量要么是锁住状态,要么就是不加锁状态,而且一次只有一个线程可以对其加锁。而读写锁可以有3种状态:读模式加锁状态、写模式加锁状态和不加锁状态。一次只有一个线程可以占有写模式的读写锁,但是多个线程可以同时占有读模式的读写锁。
当读写锁是写加锁状态时,在这个锁被释放前,所有试图对这个锁加锁的线程都会被阻塞。当读写锁在读加锁状态时,所有试图以读模式对它进行加锁的线程都可以得到访问权,但是任何希望以写模式对此锁进行加锁的线程都会阻塞,直到所有的线程释放它们的读锁为止。虽然各操作系统对读写锁的实现各不相同,但当读写锁处于读模式锁住的状态时,而这时有一个线程试图以写模式获取锁,读写锁通常会阻塞随后的读模式锁请求。这样可以避免读模式锁长期占用,而等待的写模式锁请求一直得不到满足。
读写锁适用情况
读写锁非常适合对数据读的次数远远大于写的情况。当读写锁在写模式下锁住状态时,它说保护的数据可以被安全地修改,因为一次只有一个线程可以在写模式下拥有读写锁。当读写锁处于读模式下锁住状态时,只要线程先获取了读模式下的读写锁,该锁所保护的数据就可以被多个获得读模式锁的线程读取。
读写锁也叫共享互斥锁(shared-exclusive lock)。当读写锁是读模式锁住时,就可以说成是以共享模式锁住的。当它是写模式锁住时,就可以说成是以互斥模式锁住的。
读写锁API函数
//头文件
#include <pthread.h>
#include <time.h> //struct timespec结构体
//读写锁API函数
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock); //写模式加锁函数
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock); //读模式加锁函数
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock); //解除读写锁函数
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_timedrdlock(pthread_rwlock_t *restrict rwlock, const struct timespec *restrict abs_timeout);
int pthread_rwlock_timedwrlock(pthread_rwlock_t *restrict rwlock, const struct timespec *restrict abs_timeout);
读写锁初始化/销毁
和互斥量一样,读写锁在使用之前必须初始化,在释放它们底层的内存之前必须销毁。
#include <pthread.h> int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr); 【参数】 rwlock:读写锁指针。 attr: 读写锁属性。如果参数值为NULL,表示创建一个默认属性的读写锁。 【说明】pthread_rwlock_init函数动态创建一个读写锁并初始化。 int pthread_rwlock_destroy(pthread_rwlock_t *rwlock); 【说明】销毁rwlock指针指向的读写锁。 【返回值】两个函数的返回值:成功,返回0;失败,返回错误码。 【扩展】对静态分配的读写锁进行初始化方法 pthread_rwlock_t rwlock=PTHREAD_RWLOCK_INITIALIZER;
【说明】在使用读写锁占用的内存之前,需要调用pthread_rwlock_destroy函数做清理工作。如果pthread_rwlock_init函数为读写锁分配了资源,pthread_rwlock_destroy函数将释放这些资源。如果在调用pthread_rwlock_destroy之前就释放了读写锁占用的内存空间,那么分配这个锁的资源就会丢失。
读写锁加锁/加锁
在读模式下锁定读写锁,需要调用pthread_rwlock_rdlock函数。要在写模式下锁定读写锁,需要调用pthread_rwlock_wrlock函数。不管以何种方式锁住读写锁,都可以调用pthread_rwlock_unlock函数进行解锁。
#include <pthread.h> int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock); //写模式加锁函数 int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock); //读模式加锁函数 int pthread_rwlock_unlock(pthread_rwlock_t *rwlock); //解除读写锁函数 【返回值】所有函数的返回值:成功,返回0;失败,返回错误码。
Single UNIX Specification还定义了读写锁原语的条件版本。
#include <pthread.h> int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock); int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock); 【返回值】两个函数的返回值:成功,返回0;失败,返回错误码。 【说明】这两个函数是读写锁的条件版本,可以获取锁时,返回值为0,不能获取锁时,线程不会阻塞,而是直接返回EBUSY错误码。
带有超时的读写锁
和互斥量一样,Single UNIX Specification提供了带有超时的读写锁加锁函数,使应用程序在获取读写锁时避免陷入永久阻塞状态。
#include <pthread.h> #include <time.h> int pthread_rwlock_timedrdlock(pthread_rwlock_t *restrict rwlock, const struct timespec *restrict abs_timeout); int pthread_rwlock_timedwrlock(pthread_rwlock_t *restrict rwlock, const struct timespec *restrict abs_timeout); 【返回值】两个函数的返回值:成功,返回0;失败,返回错误码。 【说明】这两个函数的行为与它们“不计时”的版本类似。abs_timeout参数指向struct timespec结构体,指定线程应该被阻塞的时间。如果线程不能获取读写锁,那么超时到期时, 这两个函数将返回ETIMEDOUT错误。与pthread_mutex_timedlock函数类似,超时指定的是绝对时间,而不是相对时间。
实例1:读写锁的使用方法。在该实例中,多个工作线程获取单个主线程分配给它们的作业,作业请求队列由读写锁保护。
1 #include <stdlib.h> 2 #include <pthread.h> 3 4 struct job { 5 struct job *j_next; 6 struct job *j_prev; 7 pthread_t j_id; /* tells which thread handles this job */ 8 /* ... more stuff here ... */ 9 }; 10 11 struct queue { 12 struct job *q_head; 13 struct job *q_tail; 14 pthread_rwlock_t q_lock; 15 }; 16 17 /* 18 * Initialize a queue. 19 */ 20 int 21 queue_init(struct queue *qp) 22 { 23 int err; 24 25 qp->q_head = NULL; 26 qp->q_tail = NULL; 27 //初始化读写锁 28 err = pthread_rwlock_init(&qp->q_lock, NULL); 29 if (err != 0) 30 return(err); 31 /* ... continue initialization ... */ 32 return(0); 33 } 34 35 /* 36 * Insert a job at the head of the queue. 37 */ 38 void 39 job_insert(struct queue *qp, struct job *jp) 40 { 41 pthread_rwlock_wrlock(&qp->q_lock); //对读写锁进行写操作加锁 42 jp->j_next = qp->q_head; //待插入作业的j_next指针指向当前队列中的第1个job结点 43 jp->j_prev = NULL; 44 if (qp->q_head != NULL) //如果队列不为空 45 qp->q_head->j_prev = jp; //当前队列头结点的j_prev指针指向待插入的job结点 46 else //如果队列为空 47 qp->q_tail = jp; //队列尾指针q_tail也指向新的job结点 48 qp->q_head = jp; //队列头指针q_head指向新插入的job结点,即新的job结点插入到了队列头中 49 pthread_rwlock_unlock(&qp->q_lock); //对读写锁进行解锁 50 } 51 52 /* 53 * Append a job on the tail of the queue. 54 */ 55 void 56 job_append(struct queue *qp, struct job *jp) 57 { 58 pthread_rwlock_wrlock(&qp->q_lock); //对读写锁进行写操作加锁 59 jp->j_next = NULL; 60 jp->j_prev = qp->q_tail; //队列尾指针指向当前尾结点的前继结点 61 if (qp->q_tail != NULL) 62 qp->q_tail->j_next = jp; //当前队列尾结点的j_next指向新结点jp 63 else 64 qp->q_head = jp; /* list was empty */ 65 qp->q_tail = jp; //队列尾指针指向新结点 66 pthread_rwlock_unlock(&qp->q_lock); 67 } 68 69 /* 70 * Remove the given job from a queue. 71 */ 72 void 73 job_remove(struct queue *qp, struct job *jp) 74 { 75 pthread_rwlock_wrlock(&qp->q_lock); 76 if (jp == qp->q_head) { //待删除结点jp是队列头结点 77 qp->q_head = jp->j_next; //队列头指针指向jp的后继结点 78 if (qp->q_tail == jp) 79 qp->q_tail = NULL; 80 else 81 jp->j_next->j_prev = jp->j_prev; //jp的后继结点的j_prev指向jp的前继结点 82 } else if (jp == qp->q_tail) { //待删除结点jp是队列尾结点 83 qp->q_tail = jp->j_prev; //队列尾指针指向jp的前继结点 84 jp->j_prev->j_next = jp->j_next; //jp的前继结点的j_next指向jp的后继节点 85 } else { 86 jp->j_prev->j_next = jp->j_next; //jp的前继结点的j_next指向jp的后继结点 87 jp->j_next->j_prev = jp->j_prev; //jp的后继结点的j_prev指向jp的前继结点 88 } 89 pthread_rwlock_unlock(&qp->q_lock); 90 } 91 92 /* 93 * Find a job for the given thread ID. 94 */ 95 struct job * 96 job_find(struct queue *qp, pthread_t id) 97 { 98 struct job *jp; 99 100 if (pthread_rwlock_rdlock(&qp->q_lock) != 0) //对读写锁进行读操作加锁 101 return(NULL); 102 103 for (jp = qp->q_head; jp != NULL; jp = jp->j_next) 104 if (pthread_equal(jp->j_id, id)) 105 break; 106 107 pthread_rwlock_unlock(&qp->q_lock); 108 return(jp); 109 }
【分析】
1.向队列中增加作业或者从队列中删除作业时,都采用了写模式来锁住队列的读写锁。
2.搜索队列时,采用了读模式来锁住队列的读写锁,允许所有的工作线程能并发地搜索队列。
3.工作线程只能从队列中读取与它们的线程ID相匹配的作业。由于作业结构体同一时间只能由一个线程使用,所以不需要额外的加锁。
4.只有在线程搜索队列中的作业的频率远远高于增加或删除作业时,使用读写锁才能改善性能。