写点什么

一个 cpp 协程库的前世今生(十六)读写锁

作者:SkyFire
  • 2022 年 1 月 11 日
  • 本文字数:3039 字

    阅读完需:约 10 分钟

一个cpp协程库的前世今生(十六)读写锁

为了防止大家找不到代码,还是先贴一下项目链接:


GitHub - skyfireitdiy/cocpp at cocpp-0.1.0


本文介绍一下读写锁的实现。


读写锁,区分读锁和写锁,读锁可以多个协程共享,都可以获取到,而写锁是独占的。


接下来看一下接口定义。

接口

接口定义位于 include/cocpp/sync/co_shared_mutex.h:



class co_shared_mutex : private co_noncopyable{private: enum class lock_type { unique, // 唯一锁 shared // 共享锁 };
struct shared_lock_context { lock_type type; // 锁类型 co_ctx* ctx; // 当前锁的持有者 bool operator==(const shared_lock_context& other) const; // 比较 };
struct lock_context_hasher { std::size_t operator()(const shared_lock_context& other) const; };
co_spinlock spinlock__; // 互斥锁 std::deque<shared_lock_context> wait_deque__; // 等待队列 std::unordered_set<shared_lock_context, lock_context_hasher> owners__; // 持有者 void wake_up_waiters__(); // 唤醒等待队列public: void lock(); // 加锁 void unlock(); // 解锁 bool try_lock(); // 尝试加锁 void lock_shared(); // 加共享锁 void unlock_shared(); // 解共享锁 bool try_lock_shared(); // 尝试加共享锁};
复制代码


加锁与解锁均有 shared 对应的接口,这里的 shared 可以理解为对读锁操作。也就是说,对与非 shared 接口来说,其意义与 co_mutex 接口的功能是一样的。


接下来看一下成员和内部的数据结构。

lock_type

锁类型,unique 表示独占锁(写者),shared 表示共享锁(读者)。

shared_lock_context

持有者的上下文,对于其他类型的互斥量来说,持有者仅保存一个 ctx 就可以了。但是对于读写所来说,持有者还需要保存其加锁类型。

lock_context_hasher

对于 shared_lock_context 的哈希实现,因为我们将持有者保存在哈希关联容器中,因此需要定义持有者的哈希类。

spinlock__

保护数据成员的自旋锁。

wait_deque__

等待队列。

owners__

持有者,注意,共享锁可以有多个持有者,因此此处使用哈希关联容器 std::unordered_set 来存储。

实现

接下来我们来看一下读写锁的实现,相对于普通的互斥锁来说更为复杂一些。

lock

lock 的实现位于 source/cocpp/sync/co_shared_mutex.cpp:


void co_shared_mutex::lock(){    auto                ctx = co::current_ctx();    shared_lock_context context {        .type = lock_type::unique,        .ctx  = ctx    };
spinlock__.lock(); CoDefer([this] { spinlock__.unlock(); }); if (!owners__.empty()) { ctx_enter_wait_state__(ctx, CO_RC_TYPE_SHARED_MUTEX, this, wait_deque__, context); lock_yield__(spinlock__, [this] { return !owners__.empty(); }); }
owners__.insert(context);}
复制代码


因为写锁具有排他性,因此是否有持有者,就是是否可以加锁的判断标准。逻辑与其他的锁类似,不做赘述。

lock_shared

lock_shared 这个实现如下(source/cocpp/sync/co_shared_mutex.cpp):


void co_shared_mutex::lock_shared(){    auto                ctx = co::current_ctx();    shared_lock_context context {        .type = lock_type::shared,        .ctx  = ctx    };
spinlock__.lock(); CoDefer([this] { spinlock__.unlock(); }); if (!owners__.empty() && (*owners__.begin()).type == lock_type::unique) { ctx_enter_wait_state__(ctx, CO_RC_TYPE_SHARED_MUTEX, this, wait_deque__, context);
lock_yield__(spinlock__, [this] { return !owners__.empty() && (*owners__.begin()).type == lock_type::unique; }); }
owners__.insert(context);}
复制代码


如果当前所被写者持有就无法获得锁,而其他情况下是可以获得获取锁。判断被写者持有便是!owners__.empty() && (*owners__.begin()).type == lock_type::unique这个条件的逻辑含义。

unlock

unlock 写者解锁,实现位于 source/cocpp/sync/co_shared_mutex.cpp:


void co_shared_mutex::unlock(){    auto                ctx = co::current_ctx();    shared_lock_context context {        .type = lock_type::unique,        .ctx  = ctx    };
spinlock__.lock(); CoDefer([this] { spinlock__.unlock(); });
if (!owners__.contains(context)) { CO_O_ERROR("ctx is not owner, this ctx is %p", ctx); throw co_error("ctx is not owner[", ctx, "]"); }
owners__.erase(context); wake_up_waiters__();}
复制代码


其需要判断互斥量是否真的被当前 ctx 持有,如果不是,会抛出异常。然后将当前协程从持有者集合中删除。调用 wake_up_waiters__来唤醒等待队列中的等待者。这里重点讲一下 wake_up_waiters__。

wake_up_waiters__

该函数定义如下:



void co_shared_mutex::wake_up_waiters__(){ if (wait_deque__.empty()) { return; }
if (!owners__.empty()) { return; }
if (wait_deque__.front().type == lock_type::unique) { wake_front__(wait_deque__, std::function([](shared_lock_context& ctx) { ctx.ctx->leave_wait_resource_state(); })); return; }
auto iter = std::remove_if(wait_deque__.begin(), wait_deque__.end(), [](auto& c) { return c.type == lock_type::shared; });
std::deque<shared_lock_context> new_owner { iter, wait_deque__.end() }; wait_deque__.erase(iter, wait_deque__.end()); for (auto& c : new_owner) { c.ctx->leave_wait_resource_state(); }}
复制代码


逻辑叙述如下:


  1. 如果等待队列为空,直接返回(没有需要唤醒的协程了)。

  2. 如果持有者非空,也直接返回。对于这种情况,对应的场景为多个读者协程共享一个锁,其中一个协程解锁了,这种情况是没必要去唤醒的,因为此时等待队列中应该是没有读者的(读者可以直接获取锁,而不用加到等待队列),而等待队列中的写者,即使唤醒了也获取不到锁,因此此处不必要唤醒。

  3. 判断等待队列中第 1 个是读者还是写者,如果是写者,就唤醒开头的一个协程。

  4. 如果是读者,需要唤醒等待队列中所有的读者。


对于共享锁的解锁以及其他接口,和其他的所逻辑都类似,不做赘述。

缺陷

此处实现的共享锁是有缺陷的,简要描述如下。


当系统中频繁发生读操作(连续的,一个读操作执行结束之前,另一个读操作就开始),会由于每一次读操作都能获得共享锁而将写操作饿死,写操作永远在等待队列中,而读操作的序列只要不结束,写操作就无法进行。


这个缺陷留在以后的版本中再做修复,涉及到锁的公平性的问题,不是很影响使用。

总结

本文描述了共享锁的实现,比其他的锁类型要稍微复杂一些,主要是要区分加锁的类型,不是很难理解。

发布于: 刚刚阅读数: 2
用户头像

SkyFire

关注

这个cpper很懒,什么都没留下 2018.10.13 加入

会一点点cpp的苦逼码农

评论

发布
暂无评论
一个cpp协程库的前世今生(十六)读写锁