WriteThread 如何控制并发写入流程
建议先阅读上一篇文章 WriteBatch 写入前的准备工作 了解一些基本概念
本篇博客讲解批量写流程的控制流程,了解这个流程之后再来讲解 PipelineWriteImpl 中优化。
整个写入流程大致如下:
WriteThread::JoinBatchGroup
JoinBatchGroup
函数充当着锁的作用:能成为 leader 的 writer 才能真正执行 WriteToWAL
和 MemTable::Add
操作,其他 writers 只能阻塞等待 leader-writer 完成。
在 WriteThread
中有个 newest_writer_
字段总是指向最新插入的 Writer
对象:
- 如果新插入一个
Writer
对象 w,则会尝试让newest_writer_
指向该 w 。如果当前触发了 WriteStall 则会等待WriteStall
被解除,才会再次尝试让newest_writer_
指向该 w - 如果 w 插入时,
newest_writer_ == NULL
,则 w 能顺利通过JoinBatchGroup
函数,进入后续写入流程 - 否则,说明当前已经存在 leader-writer,则只能阻塞等待
w->state
更改为下面其中一种才能解除阻塞:STATE_GROUP_LEADER
: 变成下一个 write_group 的 leader,执行写入STATE_COMPLETED
: 说明已经存在一个 leader-writer 替自己完成了写入,解除阻塞后就可以直接返回应用层STATE_MEMTABLE_WRITER_LEADER
: …STATE_PARALLEL_MEMTABLE_WRITER
: …
代码如下:
1 | void WriteThread::JoinBatchGroup(Writer* w) { |
WriteThread::LinkOne
LinkOne
的功能其实就是在链表头发插入一个节点,不过需要先检查一个前提 WriteStall。
WriteStall
如果写操作触发了 WriteStall ,会向 WriterThread 中写入一个 write_stall_dummy_
标志。因此在尝试将 newest_writer_
指向新加入的 w
时都需要先检查下 newest_writer_
是否等于 write_stall_dummy_
。如果等于,有以下两种情况:
Writer::no_slowdown == false
,这是默认情况,即基于条件变量stall_cv_
阻塞等待 WriteStall 解除。解除后则需要重新读取newest_writer_
条件变量一般配合 while 一起使用,防止虚假唤醒,因此
stall_cv_
被唤醒后会 continue 并进行下一轮 while 循环,确认newest_writer_
不是write_stall_dummy_
才能继续下一步。Writer::no_slowdown == true
,此时将错误Status::Incomplete
返回给上层应用,让其自行决定如何处理WriteStall
,这就类似网络编程的非阻塞行为。
compare_exchange_weak
当没有 WriteStall 或者有已经解除,则可以继续写入,将 w->link_older = writers
。这里的 link_older
的语义其实就是 next
指针,效果即 w->next = writers
。
完成这一步后 却并没有执行 writers->prev = w 操作,为什么呢?这是为了在 leader-writer 写流程结束时能通过 link_newer/prev
是否为 NULL 选出下一轮的 leader-writer,详见后文的 ExitAsBatchGroupLeader
函数。
再通过 compare_exchange_weak
操作将 newest_writer_
指向最新插入的 w,
成功,则通过判断
writers
是否为 NULL,来判断 w 是不是下一轮 writer_group 的 leader-writer这是因为上一个 write_group 完成后就会尝试将
newest_writer_
设置为 NULL,只需要通过判断writers == NULL
就可以确定新插入的 w 是不是下一轮 write_group 的 leader。失败,则说明有个并发
w0
在自己之前完成了compare_exchange_weak
操作,则自己会在下一轮 while 循环中完成此操作,使得newest_writer_
指向 w,并形成w->next = w0
连接。
每次有新的 w,都只是改变 newest_writer_
,并通过 link_older/next
指针把所有插入的 writers 串联起来,且不重不漏。由于每次只有 leader-writer 具有写权限,再让 leader-writer 在将所有的 writers 打包成一个 writer_group 时,给缺失的 link_newer/prev
指针赋值,就完成了双链表创建。由于此时实际上已经是单线程操作,因此不需要借助任何同步措施也没有并发的风险。
代码如下。
1 | bool WriteThread::LinkOne(Writer* w, std::atomic<Writer*>* newest_writer) { |
WriteThread::AwaitState
WriteThread::AwaitState
中的优化点较多,下一期单独开一篇讲解这里的优化。这个函数的作用是阻塞等待直到满足 w->state & goal_mask != 0
。
1 | uint8_t AwaitState(Writer* w, uint8_t goal_mask, AdaptationContext* ctx); |
WriteThread::CreateMissingNewerLinks
缺失的 prev
指针由 CreateMissingNewerLinks
函数补全。
传入的 head
即 newest_writer_
的值,目前所有待执行的 writers 已经通过 next 指针串联起来了,这里要做的就是将其 prev 指针补全。代码如下:
1 | void WriteThread::CreateMissingNewerLinks(Writer* head) { |
WriteThread::EnterAsBatchGroupLeader
writer 没有阻塞在 WriteThread::JoinBatchGroup
函数,则说明 writer
目前已经成为 leader-writer,则需要由 leader-writer 尝试将目前所有待执行的 writers 封装到一个 write_group 中,这个由 EnterAsBatchGroupLeader
函数完成。
执行到 EnterAsBatchGroupLeader
函数时,newest_writer_
可能一直在更改,即不断指向最新的 writer。但是没关系,因为此时 leader-writer 已经诞生了,更新的 writer 在执行 WriteThread::JoinBatchGroup
时候会被阻塞在 AwaitState
,如果此时 leader-writer 刚好执行到 EnterAsBatchGroupLeader
函数,则会由 leader-writer 将 [leader-writer, newest_writer] 区间的所有 writers 封装到 writer_group 中,由 leader-writer 来统一执行批量写入。其中 write_group->last_writer
指向的就是当前最新的 newest_writer。
在上一篇文章,讲解了 WriteGroup
内部封装了迭代器,那么就可以使用如下方式并以 leader_writer -> newst_writer
顺序遍历所有 writers。
1 | for(auto writer : *writer_group) { |
代码如下4步:
1 | size_t WriteThread::EnterAsBatchGroupLeader(Writer* leader, |
WriteThread::ExitAsBatchGroupLeader
这里暂时不关注 enable_pipelined_write_, 这是开启 PipelineWriteImpl 的标志位。
当数据都已经写入 WAL 和 MemTable,则会调用 ExitAsBatchGroupLeader
,此时需要判断在当前 write_group 写入过程中是否出现了新的 writers:
- 是:则需要从等待的 writers 中挑选出新的 leader-writer
- 否:则需要将
newest_writer_
赋值为 NULL
需要先读取 newest_writer_
的最新值 head,来判断是否有新的 writer
插入:
如果
head != last_writer
则说明在当前 write_group 写操作过程中有出现新的 writers ,并阻塞等待在JoinBatchGroup
如果
head == last_writer
但是newest_writer_.compare_exchange_strong(head, nullptr)
为 false,则说明在newest_writer_
load 之后并在compare_exchange_strong
之前 有新的 writers 出现上述两种情况,都需要将 [
last_writer
,head
] 区间所有 writers 缺失的 prev 指针补全,因为补全后last_writer->prev
指向的就是新的 leader-writer。这是因为
last_writer->prev
是在 last_writer 之后最早插入的 writer,为保持顺序性,这就是新的 leader-writer。
完成上述操作,剩下的就是将当前 write_group 中的所有 writers 状态更改为 STATE_COMPLETED 解除他们在 JoinBatchGroup
处的阻塞,尽快返回应用层。这一步也有个小细节,见代码注释。
1 | void WriteThread::ExitAsBatchGroupLeader(WriteGroup& write_group, |
By the Way
- 这篇文章里面用了许多原子操作以及一些内存序,诸如
compare_exchange_strong/compare_exchange_weak
区别,acquire/release
语义,等后面有空再单独讲解下这个问题。