C++用Mutex实现读写锁
近期答辩完成了,想回头看看之前没做过的2PL。
实现2PL有4种方式:
- 死锁检测。本篇是为了做这个而实现的,做这个事情的原因是c++标准库的shared_mutex无法从外界告知获取锁失败。
- 如果需要等待,那么马上结束txn。C++中有try_lock这样的方式,如果上锁失败就返回false,这样就可以实现这个了。
- 如果需要等待,那么杀死当前已经获得锁的一方。
- 在上锁前对资源排序。
2和4是最简单的,没什么好说的。3比1略容易一些。
基本思路
一个读写锁应该具有以下特征:
- 多个读者可以同时访问
- 写者独占访问
- 写者与读者互斥
- 避免写者饥饿或读者饥饿
- 锁的递归使用
由于实现的锁不能够出现读饿死、写饿死的现象,所以我想到一个很简单的方法:先到先得。当然也许会有其他方案。
先到先得的方式下,如何判断一个线程是否该阻塞?
- 第一个写请求之前的所有读请求可以进行
- 如果第一个请求是写请求,那么只有这一个写请求可以进行
- 如果没有写请求,那么所有读都可以进行
- 如果没有读请求,那么第一个写请求可以进行。这实际是2的特殊情况
- 其他请求都不可以进行
我们画图来说明一下。
假定某一刻有这些请求被阻塞,现在考虑挑出来可以执行的线程来执行

队列中,第一个写请求之前的读都可以进行,所以此时1,2线程是可以执行的。它们读完后释放锁,于是在这个队列中删除了1,2

6在环检测的时候被要求结束,然后线程3也结束了,所以此时所有的读都可以进行。

实现先到先得,可以通过记录正在进行读的线程数量,正在进行写的线程数量,请求写但是被阻塞的线程数量,请求读但是被阻塞的线程数量,然后根据条件来分配资源给某个线程……维护的信息数量可能不止这些,比如说需要维护哪些线程的读被阻塞了。
而环检测的2PL,我们需要在外界通知线程锁获取失败,所以选择了使用队列来实现,这个队列需要支持:
- 添加读者、写者(AddReader, AddWriter)
- 删除读者、写者(RemoveReader, RemoveWriter,为了简化,统一为一个Remove了)
- 当可以获得锁的时候,提醒可以获得锁的线程。这个可以用condition_variable实现
- 确定某个线程是否应该阻塞
然而做这样一个队列还是需要费一些功夫的。
队列实现
明确了功能需求后,考虑一下需要什么样的数据结构。普通的队列肯定是不够的,毕竟我们会删除其中任意一个元素,容易想到的是map/set。然后考虑到先到先得的顺序要求,可以考虑额外记录一个逻辑时间timestamp,每当一个请求到达,就递增timestamp。由于加入了timestamp,所以为了支持删除,至少需要tid:timestamp的映射。而为了支持按timestamp查询,至少需要timestamp:tid的映射。此外,需要记录一个请求是读还是写,所以一共需要tid:timestamp的映射和timestamp:<tid,读写标记>的映射。
timestamp:<tid,读写标记>映射关系,很容易想到通过std::map这种天然自带排序的数据结构来实现,即:
- 从最小到最大遍历开头的读请求,这部分线程可以直接执行。
- 如果是写请求开头的,那么这个写可以直接执行。
- 解锁的时候删除该线程的记录。
笔者在此前做了CMU15445,里面的GC的watermark和这个非常显相似。CMU15445中作者提到了可以使用unordered_map来将时间复杂度从O(logn)优化到O(1),这种做法我想到了,所以这里的队列使用的都是unordered_map。
#pragma once
#ifndef READER_WRITER_QUEUE_H
#define READER_WRITER_QUEUE_H
// INSPIRED BY CMU15445 fall2023 watermark
#include <cassert>
#include <unordered_map>
class ReaderWriterQueue {
public:
void AddReader(int tid) {
assert(tid_ts.count(tid) == 0);
ts_tt[next_timestamp] = {tid, TidType::kRead};
tid_ts[tid] = next_timestamp;
next_timestamp++;
}
void AddWriter(int tid) {
assert(tid_ts.count(tid) == 0);
ts_tt[next_timestamp] = {tid, TidType::kWrite};
tid_ts[tid] = next_timestamp;
next_timestamp++;
}
void Remove(int tid) {
auto ts = tid_ts.find(tid);
if (ts == tid_ts.end()) return;
assert(ts_tt.count(ts->second) == 1);
ts_tt.erase(ts->second);
tid_ts.erase(ts);
}
bool ShallBlock(int tid) {
ResetMinWriteTimestamp();
ResetMinTimestamp(); // 这两个timestamp处理可以合并
auto iter = tid_ts.find(tid);
assert(iter != tid_ts.end());
assert(ts_tt.count(iter->second) == 1);
auto ts = iter->second;
auto [_, type] = ts_tt[ts];
// 如果读者之前有写者,那么就需要阻塞等待
if (type == TidType::kRead) return ts > min_write_ts;
// 如果写者之前有读者,那么就需要阻塞等待
if (min_ts < min_write_ts) return true;
// 如果写者之前有写者,那么就需要阻塞等待
return ts_tt[min_write_ts].tid != tid;
}
private:
void ResetMinWriteTimestamp() {
for (; min_write_ts < next_timestamp; min_write_ts++) {
auto iter = ts_tt.find(min_write_ts);
if (iter == ts_tt.end()) {
continue;
} else if (iter->second.type == TidType::kWrite) {
break;
} else { // iter->second.type == TidType::kRead
continue;
}
}
}
void ResetMinTimestamp() {
for (; min_ts < next_timestamp; min_ts++) {
auto iter = ts_tt.find(min_ts);
if (iter != ts_tt.end())
break;
}
}
long next_timestamp = 0;
long min_write_ts = 0;
long min_ts = 0;
struct TidType {
int tid;
enum LockType {kRead, kWrite} type;
bool operator==(const TidType &rhs) const {
return tid == rhs.tid && type == rhs.type;
}
};
std::unordered_map<long, TidType> ts_tt;
std::unordered_map<int, long> tid_ts;
};
#endif // READER_WRITER_QUEUE_H
将队列封装为读写锁
这一步封装已经非常容易了,一个请求到来,添加到队列中。如果需要阻塞,那么就通过condition_variable等待通知。解锁的时候,不仅仅需要在队列中进行移除,还需要notify_all。notify_all还可以优化,但是这不是那么容易的事情了,不考虑。
#pragma once
#ifndef SIMPLE_SHARED_MUTEX_H
#define SIMPLE_SHARED_MUTEX_H
#include <condition_variable>
#include <ctime>
#include <cstdio>
#include <mutex>
#include <unistd.h>
#include "reader_writer_queue.h"
class SimpleSharedMutex {
public:
void lock() {
std::unique_lock lock{mtx};
auto tid = ::gettid();
queue.AddWriter(tid);
while (queue.ShallBlock(tid)) cv.wait(lock);
// printf("lock %d\n", tid);
}
void shared_lock() {
std::unique_lock lock{mtx};
auto tid = ::gettid();
queue.AddReader(tid);
while (queue.ShallBlock(tid)) cv.wait(lock);
// printf("slock %d\n", tid);
}
void unlock() {
std::unique_lock lock{mtx};
queue.Remove(::gettid());
cv.notify_all();
// printf("ulock %d\n", ::gettid());
}
void shared_unlock() {
std::unique_lock lock{mtx};
queue.Remove(::gettid());
cv.notify_all();
// printf("uslock %d\n", ::gettid());
}
private:
std::mutex mtx;
ReaderWriterQueue queue;
std::condition_variable cv;
};
#endif // SIMPLE_SHARED_MUTEX_H
C++用Mutex实现读写锁的更多相关文章
- Linux的线程同步对象:互斥量Mutex,读写锁,条件变量
进程是Linux资源分配的对象,Linux会为进程分配虚拟内存(4G)和文件句柄等 资源,是一个静态的概念.线程是CPU调度的对象,是一个动态的概念.一个进程之中至少包含有一个或者多个线程.这 ...
- Go基础系列:互斥锁Mutex和读写锁RWMutex用法详述
sync.Mutex Go中使用sync.Mutex类型实现mutex(排他锁.互斥锁).在源代码的sync/mutex.go文件中,有如下定义: // A Mutex is a mutual exc ...
- Go语言中的互斥锁和读写锁(Mutex和RWMutex)
目录 一.Mutex(互斥锁) 不加锁示例 加锁示例 二.RWMutex(读写锁) 并发读示例 并发读写示例 三.死锁场景 1.Lock/Unlock不是成对出现 2.锁被拷贝使用 3.循环等待 虽然 ...
- Golang 入门系列(十六)锁的使用场景主要涉及到哪些?读写锁为什么会比普通锁快
前面已经讲过很多Golang系列知识,感兴趣的可以看看以前的文章,https://www.cnblogs.com/zhangweizhong/category/1275863.html, 接下来要说的 ...
- go Mutex (互斥锁)和RWMutex(读写锁)
转载自: https://blog.csdn.net/skh2015java/article/details/60334437 golang中sync包实现了两种锁Mutex (互斥锁)和RWMute ...
- Golang 读写锁RWMutex 互斥锁Mutex 源码详解
前言 Golang中有两种类型的锁,Mutex (互斥锁)和RWMutex(读写锁)对于这两种锁的使用这里就不多说了,本文主要侧重于从源码的角度分析这两种锁的具体实现. 引子问题 我一般喜欢带着问题去 ...
- Go 互斥锁(sync.Mutex)和 读写锁(sync.RWMutex)
什么时候需要用到锁? 当程序中就一个线程的时候,是不需要加锁的,但是通常实际的代码不会只是单线程,所以这个时候就需要用到锁了,那么关于锁的使用场景主要涉及到哪些呢? 多个线程在读相同的数据时 多个线程 ...
- 第8章 用户模式下的线程同步(3)_Slim读写锁(SRWLock)
8.5 Slim读/写锁(SRWLock)——轻量级的读写锁 (1)SRWLock锁的目的 ①允许读者线程同一时刻访问共享资源(因为不存在破坏数据的风险) ②写者线程应独占资源的访问权,任何其他线程( ...
- Windows平台下的读写锁
Windows平台下的读写锁简单介绍Windows平台下的读写锁以及实现.背景介绍Windows在Vista 和 Server2008以后才开始提供读写锁API,即SRW系列函数(Initialize ...
- 嵌入式 Linux线程同步读写锁rwlock示例
读写锁比mutex有更高的适用性,可以多个线程同时占用读模式的读写锁,但是只能一个线程占用写模式的读写锁.1. 当读写锁是写加锁状态时,在这个锁被解锁之前,所有试图对这个锁加锁的线程都会被阻塞:2. ...
随机推荐
- NCS开发学习笔记-基础篇-第 1 课 – nRF Connect SDK 简介
第 1 课 – nRF Connect SDK 简介 目标 了解 nRF Connect SDK 的结构和内容 在内部,nRF Connect SDK 代码分为四个主要存储库: nrf – 应用程序. ...
- ABAQUS 中的一些约定
目录 自由度notation Axisymmetric elements Activation of degrees of freedom Internal variables in Abaqus/S ...
- 后台返回文件URL,前端下载文件,即使设置文件名,下载的文件名称并不是自己所设置的问题
1.背景 项目中遇到这么一个问题,上传文件后,后台返回的是一个URL,前端需要通过点击下载这个文件 2.首次处理 当时一看是下载文件,觉得很简单,无非是通过创建a标签来实现,以下是我当时的代码,用的是 ...
- python实现排列组合--itertools
这是一个python自带的工具集,简单好用功能强大,能够大大提升编写代码效率. 功能不止排列组合,其他的用用加深理解了再整理. 官方文档:https://docs.python.org/zh-cn/3 ...
- Docker中应用的性能调优指南(一)- 先谈谈容器化性能调优
前言 性能调优是一个老生常谈的话题,通常情况下,一个应用在上线之前会进行容量规划.压力测试并进行验证,而性能调优则是在容量规划与验证结果之间出现差异时会进行的必然手段.从某种角度来讲,性能调优是一个非 ...
- MySQL-事务中的一致性读和锁定读的具体原理
前言 上一篇文章MySQL-InnoDB行锁中,提到过一致性锁定读和一致性非锁定读,这篇文章会详细分析一下在事务中时,具体是如何实现一致性的. 一致性读原理 start transaction和beg ...
- 接口中的成员特点、类和接口之间的各种关系--java进阶day02
1.接口的成员特点 1.接口没有构造方法 接口没有构造方法,但是实现类中有构造方法,super()又该访问谁呢? 类实现接口只是认干爹,类本身还是会有亲爹Object,super()会访问Object ...
- 跳转程序控制语句:break、continue 以及死循环、标号
1.break:结束循环,结束switch语句 . 案例:模拟用户登录密码,一共三次机会,初识密码为123456 我们之前学的方法可以完成这个案例,但是这种写法还存在问题 如图 明明已经输入了正确的密 ...
- 新建一个空的 ASP.NET Core Web Application
前言 Visual Studio 2017 下操作 1. 新建项目 2. 新建空的 ASP.NET Core Web Application 确定后,需要一小点的时间等待依赖库载入... 3. 新建完 ...
- GStreamer开发笔记(一):GStreamer介绍,在windows平台部署安装,打开usb摄像头对比测试
前言 当前GStreamer是开源的多媒体框架,其适配后可以支持板卡的硬编码.硬解码,还提供RTSP服务器等功能,降低了音视频开发的门槛(转移到gstreamer配置和开发上了,但是跨平台),瑞芯 ...