> 文章列表 > C++模拟实现读写锁

C++模拟实现读写锁

C++模拟实现读写锁

文章目录

  • 一、读者写者问题
  • 二、读写锁
    • 1.读写锁的概念
    • 2.读写锁的设计
      • (1)成员变量
      • (2)构造函数和析构函数
      • (3)readLock函数
      • (4)readUnlock函数
      • (5)writeLock函数
      • (6)writeUnlock函数
    • 3.RWLock类代码
  • 三、测试读写锁

一、读者写者问题

在编写多线程的时候,有一种情况是非常常见的,那就是有些公共数据,它们被修改的机会很少,但是被读取的机会却很多。通常来说,在读取的时候会伴随着查找等操作,如果按照正常地加锁方式给这种场景下的代码加锁,会极大地降低我们的效率。所以我们可以设计读写锁来解决读者写者问题。

二、读写锁

1.读写锁的概念

我们可以举一个生活中的小例子来类比理解一下读写锁:

比如在班上要出黑板报的时候,黑板就是临界资源,负责在黑板上写内容的同学就是写者,而我们这些来来往往观看黑板报的吃瓜群众就是写者。那么我们可以确定下面几种关系:

  1. 有同学在写黑板报的时候,读黑板报的同学必须等写黑板报的同学写完才可以开始读;而有同学在读黑板报的时候,写黑板报的同学必须等读黑板报的同学读完才可以开始写。
  2. 黑板报一次只能允许有一个同学在写,如果多个同学同时写黑板报可能会覆盖其他同学的内容。
  3. 读黑板报的同学可以同时一起读,并没有任何限制。

上面的关系可以类比成读者写者问题,所以读写锁要保证的关系是:读者和写者互斥、写者和写者互斥、读者和读者没有关系不受限制。

当前锁状态 读锁请求 写锁请求
无锁 可以 可以
读锁 可以 不可以
写锁 不可以 不可以

2.读写锁的设计

基于上述介绍的关系,我们可以利用互斥锁和信号量设计出一套读写锁机制:

核心思路就是使用引用计数,当有读者申请成功读锁以后,读者计数器加一。当写者申请成功写锁以后,写者计数器加一。

(1)成员变量

首先我们这个类需要一个互斥锁和一个条件变量,然后我们需要一个计数器rd_cnt记录当前读者的数量,另一个计数器wr_cnt记录当前写者的数量。

pthread_mutex_t mtx;
pthread_cond_t cond;
int rd_cnt; // 等待读的数量
int wr_cnt; // 等待写的数量

(2)构造函数和析构函数

构造函数用来初始化计数器、互斥锁和条件变量,析构函数用来释放资源。

RWLock() : rd_cnt(0), wr_cnt(0)
{pthread_mutex_init(&mtx, nullptr);pthread_cond_init(&cond, nullptr);
}
~RWLock()
{pthread_cond_destroy(&cond);pthread_mutex_destroy(&mtx);
}

(3)readLock函数

readLock函数是加读锁函数,首先进来需要先给它加锁,然后循环判断当前是否还有写者在进行写操作,如果是那么读者就要等待写者的条件变量,等到写者完成了写操作才能继续读。当循环条件不满足时,意味着写者完成了写操作,读者被唤醒,读者数量加一,最后释放锁,就可以执行读取操作了。

这里的加锁解锁目的是保证rd_cnt++这一语句的原子性,防止多个读者同时对rd_cnt进行加一操作而出现错误。所以这里是可以保证读者和读者之间不受限制的,因为外部调用readLock函数成功之后才可以开始进行读取操作,除非当前有写者在进行写操作,否则读者不会阻塞。

而读者和写者之间的互斥关系是通过循环判断来实现的,如果当前有写者正在写数据,readLock函数就会阻塞在pthread_cond_wait(&cond, &mtx);这条语句中,只有执行完这条语句readLock函数才能继续向下执行,执行完readLock函数之后读者才能进行读操作。

void readLock()
{pthread_mutex_lock(&mtx);// cout << "加读锁成功" << endl;// 如果当前还有写者在进行写操作// 那么读者应该等待写者的条件变量// 写者完成了写操作才能读while (wr_cnt > 0)pthread_cond_wait(&cond, &mtx);// 读者数量加一rd_cnt++;pthread_mutex_unlock(&mtx);// cout << "解读锁成功" << endl;
}

(4)readUnlock函数

在读者读取完数据之后就应该调用readUnlock函数释放读锁,readUnlock函数首先也是需要加锁保证rd_cnt--这一语句的原子性。然后还需要判断一下当前读者的数量是否为0,如果是的话,就唤醒正在等待的写者,可以开始写操作了。

void readUnlock()
{pthread_mutex_lock(&mtx);// cout << "unlock 加读锁成功" << endl;// 读者数量减一rd_cnt--;// 如果当前读者的数量为0,就唤醒正在等待的写者if (rd_cnt == 0)pthread_cond_signal(&cond);pthread_mutex_unlock(&mtx);// cout << "unlock 解读锁成功" << endl;
}

(5)writeLock函数

writeLock函数是加写锁的函数,同样地,它也需要先加锁保证语句wr_cnt++语句的原子性。然后需要循环判断当前是否有读者或者有其它的写者在进行读写操作,如果有的话,只要有一个读者或者有一个写者,当前这个写者都不能进行写操作,必须等待其它读者或者写者完成之后才能进行。

void writeLock()
{pthread_mutex_lock(&mtx);// cout << "加写锁成功" << endl;// 如果当前有读者或者有其它写者在进行读写操作时// 当前的写者都不能进行写操作,必须等待while (wr_cnt >= 1 || rd_cnt >= 1)pthread_cond_wait(&cond, &mtx);// 走到这里,一定是当前既没有读者也没有写者在进行读写操作了// 此时当前的写者才能进行写操作// 写者数量加一wr_cnt++;pthread_mutex_unlock(&mtx);// cout << "解写锁成功" << endl;}

(6)writeUnlock函数

writeUnlock函数是解写锁函数,与前面的几个函数接口类似,只不过它需要判断一下当前的写者数量是否为0,如果是的话,就要唤醒另一个正在等待条件变量的写者。

void writeUnlock()
{pthread_mutex_lock(&mtx);// cout << "unlock 加写锁成功" << endl;// 写者数量减一wr_cnt--;// 如果当前写者数量为0,则唤醒正在等待的另一个写者if (wr_cnt == 0)pthread_cond_signal(&cond);pthread_mutex_unlock(&mtx);// cout << "unlock 解写锁成功" << endl;
}

3.RWLock类代码

#pragma once
#include <pthread.h>using namespace std;// 写者和写者之间是互斥关系
// 写者和读者之间是互斥关系
// 读者和读者之间没有关系class RWLock
{
private:pthread_mutex_t mtx;pthread_cond_t cond;int rd_cnt; // 等待读的数量int wr_cnt; // 等待写的数量public:RWLock() : rd_cnt(0), wr_cnt(0){pthread_mutex_init(&mtx, nullptr);pthread_cond_init(&cond, nullptr);}~RWLock(){pthread_cond_destroy(&cond);pthread_mutex_destroy(&mtx);}void readLock(){pthread_mutex_lock(&mtx);// cout << "加读锁成功" << endl;// 如果当前还有写者在进行写操作// 那么读者应该等待写者的条件变量// 写者完成了写操作才能读while (wr_cnt > 0)pthread_cond_wait(&cond, &mtx);// 读者数量加一rd_cnt++;pthread_mutex_unlock(&mtx);// cout << "解读锁成功" << endl;}void readUnlock(){pthread_mutex_lock(&mtx);// cout << "unlock 加读锁成功" << endl;// 读者数量减一rd_cnt--;// 如果当前读者的数量为0,就唤醒正在等待的写者if (rd_cnt == 0)pthread_cond_signal(&cond);pthread_mutex_unlock(&mtx);// cout << "unlock 解读锁成功" << endl;}void writeLock(){pthread_mutex_lock(&mtx);// cout << "加写锁成功" << endl;// 如果当前有读者或者有其它写者在进行读写操作时// 当前的写者都不能进行写操作,必须等待while (wr_cnt >= 1 || rd_cnt >= 1)pthread_cond_wait(&cond, &mtx);// 走到这里,一定是当前既没有读者也没有写者在进行读写操作了// 此时当前的写者才能进行写操作// 写者数量加一wr_cnt++;pthread_mutex_unlock(&mtx);// cout << "解写锁成功" << endl;}void writeUnlock(){pthread_mutex_lock(&mtx);// cout << "unlock 加写锁成功" << endl;// 写者数量减一wr_cnt--;// 如果当前写者数量为0,则唤醒正在等待的另一个写者if (wr_cnt == 0)pthread_cond_signal(&cond);pthread_mutex_unlock(&mtx);// cout << "unlock 解写锁成功" << endl;}
};

三、测试读写锁

我们用代码随机生成一份测试用例,测试数据文件包括 n 行测试数据,分别描述创建的 n 个线程是读者还是写者,以及读写操作的开始时间和持续时间。

每行测试数据包括四个字段,各个字段间用空格分隔。

第一字段为一个正整数,表示线程序号。

第二字段表示相应线程角色,R表示读者,W表示写者。

第三字段为一个正数,表示读写操作的开始时间,线程创建后,延迟相应时间(单位为秒)后发出对共享资源的读写申请。

第四字段为一个正数,表示读写操作的持续时间。当线程读写申请成功后,开始对共享资源的读写操作,该操作持续相应时间后结束,并释放共享资源。

测试数据随机生成,为简化起见,假设每个线程只执行一次读或写操作,之后运行结束。

下面是一个测试数据文件的例子:

C++模拟实现读写锁

测试代码:

#include <iostream>
#include <vector>
#include <string>
#include <set>
#include <cstdlib>
#include <cmath>
#include <ctime>
#include <pthread.h>
#include <unistd.h>
#include "ReaderAndWriter.hpp"using namespace std;RWLock rwlock; // 定义全局的读写锁struct ThreadData
{int _id;vector<int> _continue_time;ThreadData(int id, vector<int> &continue_time): _id(id), _continue_time(continue_time){}
};// 临界资源
// 读者负责读取,写者负责加一
int value = 100;volatile static int count = 0; // 用来记录每个线程是否执行完毕pthread_mutex_t count_mutex = PTHREAD_MUTEX_INITIALIZER;void *reader(void *args)
{// 设置线程分离pthread_detach(pthread_self());ThreadData *thread_data = (ThreadData *)args;// 加读锁cout << "线程" << thread_data->_id + 1 << "发出读操作申请" << endl;rwlock.readLock();cout << "线程" << thread_data->_id + 1 << "开始读操作, value: " << value << endl;sleep(thread_data->_continue_time[thread_data->_id]);rwlock.readUnlock();cout << "线程" << thread_data->_id + 1 << "结束读操作" << endl;// 走到这里说明当前的读操作已经结束了,count加一,当count==n的时候说明所有线程已经执行完毕pthread_mutex_lock(&count_mutex);count++;pthread_mutex_unlock(&count_mutex);
}void *writer(void *args)
{// 设置线程分离pthread_detach(pthread_self());ThreadData *thread_data = (ThreadData *)args;// 加写锁cout << "线程" << thread_data->_id + 1 << "发出写操作申请" << endl;rwlock.writeLock();cout << "线程" << thread_data->_id + 1 << "开始写操作" << endl;value++;sleep(thread_data->_continue_time[thread_data->_id]);rwlock.writeUnlock();cout << "线程" << thread_data->_id + 1 << "结束写操作" << endl;// 走到这里说明当前的读操作已经结束了,count加一,当count==n的时候说明所有线程已经执行完毕pthread_mutex_lock(&count_mutex);count++;pthread_mutex_unlock(&count_mutex);
}void createReader(int id, vector<int> continue_time)
{pthread_t tid;ThreadData *thread_data = new ThreadData(id, continue_time);pthread_create(&tid, nullptr, reader, (void *)thread_data);cout << "线程" << id + 1 << "创建成功" << endl;
}void createWriter(int id, vector<int> continue_time)
{pthread_t tid;ThreadData *thread_data = new ThreadData(id, continue_time);pthread_create(&tid, nullptr, writer, (void *)thread_data);cout << "线程" << id + 1 << "创建成功" << endl;
}int main()
{srand(time(0));// n个测试用例int n;cin >> n;// 随机生成测试用例// 1.随机生成角色char stlect_str[2] = {'R', 'W'};vector<char> role;role.resize(n);for (int i = 0; i < n; i++){int num = rand() % 2;role[i] = stlect_str[num];}// 2.随机生成开始读写时间set<int> begin_time;while (begin_time.size() < n){// 限制随机数在20s以内int num = rand() % 20;begin_time.insert(num);}// 3.随机生成读写持续时间vector<int> continue_time;continue_time.resize(n);for (int i = 0; i < n; i++){// 持续时间限制在5s以内int num = rand() % 5 + 1;continue_time[i] = num;}set<int>::iterator iter = begin_time.begin();for (int i = 0; i < n; i++){cout << i + 1 << "\\t" << role[i] << "\\t" << *iter << "\\t" << continue_time[i] << endl;iter++;}// 记录当前系统的时间time_t begin = time(nullptr);int id = 0;const double eps = 1e-8;iter = begin_time.begin();while (id < n){time_t end = time(nullptr);// 到达开始时间,创建线程if (abs((*iter) - difftime(end, begin)) < eps){// 创建读者线程if (role[id] == 'R'){createReader(id, continue_time);}// 创建写者线程else{createWriter(id, continue_time);}id++;iter++;}}//while (true){if(count == n){cout << "所有线程均已完成读写操作,主线程退出" << endl;break;}}return 0;
}

运行结果一:

C++模拟实现读写锁

运行结果二:

C++模拟实现读写锁