为了防止大家找不到代码,还是先贴一下项目链接:
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 个是读者还是写者,如果是写者,就唤醒开头的一个协程。
如果是读者,需要唤醒等待队列中所有的读者。
对于共享锁的解锁以及其他接口,和其他的所逻辑都类似,不做赘述。
缺陷
此处实现的共享锁是有缺陷的,简要描述如下。
当系统中频繁发生读操作(连续的,一个读操作执行结束之前,另一个读操作就开始),会由于每一次读操作都能获得共享锁而将写操作饿死,写操作永远在等待队列中,而读操作的序列只要不结束,写操作就无法进行。
这个缺陷留在以后的版本中再做修复,涉及到锁的公平性的问题,不是很影响使用。
总结
本文描述了共享锁的实现,比其他的锁类型要稍微复杂一些,主要是要区分加锁的类型,不是很难理解。
评论