> 文章列表 > 读写锁的原理与实现

读写锁的原理与实现

读写锁的原理与实现

文章目录

  • 什么是读写锁
    • 生产消费模型 VS 读写模型
  • 读写锁的pthread库接口
  • 模拟实现读写锁
    • 思路1——用两个锁来实现(读者优先)
      • 模拟实现
    • 思路2——两个条件变量一个锁(写者优先)
      • 模拟实现

可以看看之前写的文章线程里面有关于锁和生产消费者模型的介绍

什么是读写锁

生产消费模型 VS 读写模型

对于生产者和消费者模型的概念图👇
读写锁的原理与实现
那么我们读写锁源自读者写着模型,和生产者和消费者模型十分类似👇
读写锁的原理与实现
两个的模型实际上是一模一样的,我们还是通过“三二一”来深入了解一下读者写者模型

  • 三种关系
    • 读者-读者:没有关系,共享公共资源
    • 读者-写者:互斥、同步,因为读的时候发生了写或写的时候来读,读取到的信息都是错误的!!
    • 写者-写者:互斥,临界资源中当有一个写者时其他所有人(不论你是读者还是写者)都进不来
  • 两者角色:写者和读者
  • 一个场所:临界区

生产者和写者
两个是一模样的,都是向临界区输入数据的

读者和消费者
消费者会从临界区中拿走数据(修改临界区) 而读者不会(可以理解为对临界区表现为只读)

读写锁的pthread库接口

int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);   /* 销毁RW lock */
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,const pthread_rwlockattr_t *restrict attr);       /* 初始化RW lock */pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;   /* 直接赋值方式初始化RW lock */int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);    /* 取得读锁,进入read-mode */
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock); /* 尝试取得读锁,失败立即返回  */int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock); /* 取得写锁,进入write-mode */
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);    /* 尝试取得写锁,失败立即返回  */int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);    /* 释放读/写锁 */

这些接口与普通锁的接口类似就不一一介绍了,但是我们要明确的是不管你是读者和写者都是对一把读写锁加锁,不同身份的人通过不同的接口给读写锁加锁或解锁

  • 如果你是读者,你就用int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock); 这个接口加锁,解锁以此类推
  • 如果你是写者,你就用int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock); 这个接口加锁,解锁以此类推

读者&&写者模式

关于读写锁要知道读写锁的两个模式:

  • read_mode : 再此模式下,第一个读者取得了读写锁就进入该模式,所有写者都会被阻塞,而读者可以申请到锁,当最后一个读者释放锁,退出该模式
  • write_mode :当一个写者取得读写锁就进入该模式之,所有读者和写者均被阻塞,当申请锁的写者释放锁之后退出该模式

模拟实现读写锁

读写锁的实现的思路最重要的是,不管你的身份是读者还是写者,一定要搞清楚目前临界区域的状态!
我们把状态分为三种:

  • 读模式
  • 写模式
  • 初始状态:既不是读模式,也不是写模式。

思路1——用两个锁来实现(读者优先)

实现思路:两把锁,一把我们叫读锁、一把叫写锁。读者走读锁的门,写者走写锁的门
读写锁的原理与实现

  • 读者申请锁 和 释放锁
    - 第一个读者:先申请读锁进入临界区,这时他要申请写锁(如果成功这时正式进入读模式),这样写者就就无法进入临界区达到读者写者互斥。但是读者走的时候要释放读锁,否则其他读者进不来
    - 中间的读者:除了申请写锁(如果申请写锁就会造成死锁),其他和上面一模一样
    - 最后一个读者:除了最后一步释放锁的时候要把写锁归还,其他和上面一样

  • 写者申请锁 和 释放锁:就是直接申请写锁,释放就是直接释放写锁。

思考:这样的设计有什么缺点?
这样的设计实际上是读者优先,读者只要申请到锁,写者必须等待所有读者读完,很容易造成饥饿问题!思路二就设计一个写者优先的读写锁。

模拟实现

代码仓库version1

#include <pthread.h>
// 使用两个mutex来实现读写锁
namespace sht
{class sht_rwlock{private:pthread_mutex_t m1; // 用来锁读者 目的是为了保护read_num这个临界资源pthread_mutex_t m2; // 用来锁写者 用来控制读者之间的互斥int read_num = 0;   // 记录读者的数量int cur_state = 0;// 0 : 表示没有上锁// 1 :当前在读模式// -1 :当前在写模式public:// 初始化int sht_rwlock_init(){pthread_mutex_init(&m1, nullptr);pthread_mutex_init(&m2, nullptr);return 0;}int sht_rwlock_destroy(){pthread_mutex_destroy(&m1);pthread_mutex_destroy(&m2);}int sht_rwlock_rdlock(){pthread_mutex_lock(&m1);read_num++;if (read_num == 1)pthread_mutex_lock(&m2);// 改变成读者状态必须一定要放在申请到读者锁之后!!!!!!!!cur_state = 1;// 读者之间不是互斥关系,所以要解锁,否则其他读者进不来pthread_mutex_unlock(&m1);return 0;}int sht_rwlock_tryrdlock(){int ret1 = pthread_mutex_trylock(&m1);if (ret1 != 0)return -1;if (read_num == 1){int ret2 = pthread_mutex_trylock(&m2);if (ret2 != 0)return -1;}read_num++;cur_state = 1;pthread_mutex_unlock(&m1);return 0;}int sht_rwlock_wrlock(){int ret = pthread_mutex_lock(&m2);// 如果能走到这一步代表:读者申请到锁了,而读者和所有人互斥,所以我们改变状态并不归还锁cur_state = -1;return ret;}int sht_rwlock_trywrlock(){int ret = pthread_mutex_lock(&m2);if (ret != 0)return -1;// 如果能走到这一步代表:读者申请到锁了,而读者和所有人互斥,所以我们改变状态并不归还锁cur_state = -1;return ret;}int sht_rwlock_unlock(){if (cur_state == -1){// 写者还锁pthread_mutex_unlock(&m2);cur_state = 0;}else if (cur_state == 1){// 读者还锁pthread_mutex_lock(&m1);if (read_num == 1)pthread_mutex_unlock(&m2);read_num--;pthread_mutex_unlock(&m1);}elsereturn -1;return 0;}};} // namespace sht

思路2——两个条件变量一个锁(写者优先)

        pthread_cond_t reader_cond;  //读者队列pthread_cond_t writer_cond;   //写者队列pthread_mutex_t m;int reader_num; // 读者等待的数量int writer_num; // 写者等待的数量int cur_state;// 1 : 代表读者模式// 0 : 代表锁是为获取的// -1 :代表写者模式
  • 申请读锁的思路:
    读写锁的原理与实现

  • 申请写锁的思路:

读写锁的原理与实现


  • 释放锁的思路

读写锁的原理与实现

模拟实现

代码仓库 version2

#include <pthread.h>// 写者优先的锁
namespace sht
{class sht_rwlock{public:int sht_rwlock_init(){pthread_cond_init(&reader_cond, nullptr);pthread_cond_init(&writer_cond, nullptr);pthread_mutex_init(&m, nullptr);}int sht_rwlock_destroy(){pthread_cond_destroy(&reader_cond);pthread_cond_destroy(&writer_cond);pthread_mutex_destroy(&m);}int sht_rwlock_rdlock(){pthread_mutex_lock(&m);if (cur_state == 1){// 当前在读模式,因为我们是写者优先所以我们看看是否有写者线程在等待// 如果有我们就先执行写者,让读者去排队// 如果没有,那么读锁就申请成功while (writer_num > 0){reader_num++;pthread_cond_wait(&reader_cond, &m);reader_num--;}}else if (cur_state == -1){// 如果当前是写者模式,读者直接去排队reader_num++;pthread_cond_wait(&reader_cond, &m);reader_num--;}else{cur_state = 1;}pthread_mutex_unlock(&m);}int sht_rwlock_tryrdlock(){pthread_mutex_lock(&m);if (cur_state == 0 || cur_state == 1 && writer_num == 0){pthread_mutex_unlock(&m);return 0;}return -1;}int sht_rwlock_wrlock(){pthread_mutex_lock(&m);if (cur_state == -1){writer_num++;pthread_cond_wait(&writer_cond, &m);writer_num--;}else if (cur_state == 1){writer_num++;pthread_cond_wait(&writer_cond, &m);writer_num--;}else{// 把状态改成写者模式cur_state = -1;}pthread_mutex_unlock(&m);return 1;}int sht_rwlock_trywrlock(){pthread_mutex_lock(&m);if (cur_state == -1){pthread_mutex_unlock(&m);return 0;}return -1;}int sht_rwlock_unlock(){pthread_mutex_init(&m, nullptr);if (cur_state == 1){if (writer_num > 0){cur_state = -1;pthread_cond_signal(&writer_cond);}else{if (reader_num > 0){pthread_cond_signal(&reader_cond);}elsecur_state = 0; // 这一步一定不能丢,如果当前两个队列均为空时,要置为初始状态// 不然线程就会卡住}}else if (cur_state == -1){// 写者来还锁,如果写队列不为空 ,唤醒写队列中的写者(写者优先)// 如果写者队列空了,但是读者队列不为空,唤醒读者// 两个队列都为空,把状态置成初始状态if (writer_num > 0){pthread_cond_signal(&writer_cond);}else{if (reader_num > 0){cur_state = 1;pthread_cond_signal(&reader_cond);}elsecur_state = 0;}}else{return -1;}pthread_mutex_unlock(&m);}private:pthread_cond_t reader_cond;pthread_cond_t writer_cond;pthread_mutex_t m;int reader_num; // 读者等待的数量int writer_num; // 写者等待的数量int cur_state;// 1 : 代表读者模式// 0 : 代表锁是为获取的// -1 :代表写者模式};
} // namespace sht