写入流程
- 先加锁
- 往队列里加入数据(有可能有别的线程也加入数据)
- wait等待队首数据的线程被唤醒(此时其它数据可以写入队列)
while (!w.done && &w != writers_.front()) {
w.cv.Wait();
}
- 持有队首数据的线程被唤醒,获取最新的sequence
- 获取当前队列里的最后一个数据
- 将当前队首至队尾的数据(为了批量写入)整合
- 解锁(此时后面的线程又可以写入队列,因为队首到队尾的数据已经被缓存),且第一个插入的线程为新的队首,wait等待唤醒(步骤2)
- 插入数据
- 上锁操作队列,将此线程队首至队尾的数据都置为已读,并且唤醒相关数据的线程(这样唤醒的时候发现w.done,自己的数据已经和别人的一起写进去了,所以直接返回)
- 唤醒此时新的队首
leveldb多线程写的关键
经过系统性的分析,我们了解到leveldb实现高性能安全读写的几个关键点:
- 利用队列将写入线程排队有序执行,写操作实现了逻辑上的单线程操作;
- 在写入文件和MemTable过程是无锁状态,此时可以同时写入和读取数据,合并多个数据写入进一步提升写入性能;
- 利用原子指针代替锁避免了锁本身带来的线程切换开销;
- 如果通过迭代器遍历节点时,因为写入和读取指针都是原子的,所以也不存在安全问题;
同时读写为何是安全的
- 整个写入过程操作链表的时候有两个步骤,分别是先让节点指向后向节点,然后再让前向节点指向自己,第一步因为没有实际操作链表,所以本身就是安全的,只有第二步执行的过程如果线程切换或者同时读取(毕竟都是多核的机器)才会有可能存在不安全的可能;
- 无论是写入还是读取(通过迭代器顺序读取除外),都是先要seek,即定位,seek是从高到低方式访问链表逐渐逼近期望节点,而节点插入是从低到高插入链表,一旦seek过程访问了还没有插入完毕的节点时,该节点的低于当前高度的链表已经插入完毕,所以也不存在安全问题;
- 单向链表插入实际上只有一步操作,只要这个操作是原子的可以保证安全。
- 因为MemTable没有删除操作,永远是添加操作,也进一步巩固了该设计方案