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