内存顺序(Memory Order)

查了忘,忘了差,直接盗一篇留个地儿
这篇文章主要介绍内存顺序(Memory Order),然后会结合 RocksDB | LevelDB 中的 SkipList 源码来具体分析 RocksDB SkipList 如何通过内存顺序和原子操作做到无锁并发(一写多读)。

Memory Order

内存顺序描述了计算机 CPU 获取内存的顺序,内存的排序既可能发生在编译器编译期间,也可能发生在 CPU 指令执行期间。

为了尽可能地提高计算机资源利用率和性能,编译器会对代码进行重新排序, CPU 会对指令进行重新排序、延缓执行、各种缓存等等,以达到更好的执行效果。当然,任何排序都不能违背代码本身所表达的意义,并且在单线程情况下,通常不会有任何问题。

但是在多线程环境下,比如无锁(lock-free)数据结构的设计中,指令的乱序执行会造成无法预测的行为。所以我们通常引入内存栅栏(Memory Barrier)这一概念来解决可能存在的并发问题。

Memory Barrier

内存栅栏是一个令 CPU 或编译器在内存操作上限制内存操作顺序的指令,通常意味着在 barrier 之前的指令一定在 barrier 之后的指令之前执行。

在 C11/C++11 中,引入了六种不同的 memory order,可以让程序员在并发编程中根据自己需求尽可能降低同步的粒度,以获得更好的程序性能。这六种 order 分别是:

relaxed, acquire, release, consume, acq_rel, seq_cst

*memory_order_relaxed: *只保证当前操作的原子性,不考虑线程间的同步,其他线程可能读到新值,也可能读到旧值。比如 C++ shared_ptr 里的引用计数,我们只关心当前的应用数量,而不关心谁在引用谁在解引用。

memory_order_release:(可以理解为 mutex 的 unlock 操作)

  1. 写入施加 release 语义(store),在代码中这条语句前面的所有读写操作都无法被重排到这个操作之后,即 store-store 不能重排为 store-store, load-store 也无法重排为 store-load
  2. 当前线程内的所有写操作,对于其他对这个原子变量进行 acquire 的线程可见
  3. 当前线程内的与这块内存有关所有写操作,对于其他对这个原子变量进行 consume 的线程可见

*memory_order_acquire: *(可以理解为 mutex 的 lock 操作)

  1. 读取施加 acquire 语义(load),在代码中这条语句后面所有读写操作都无法重排到这个操作之前,即 load-store 不能重排为 store-load, load-load 也无法重排为 load-load
  2. 在这个原子变量上施加 release 语义的操作发生之后,acquire 可以保证读到所有在 release 前发生的写入,举个例子:
c = 0;

thread 1:
{
  a = 1;
  b.store(2, memory_order_relaxed);
  c.store(3, memory_order_release);
}

thread 2:
{
  while (c.load(memory_order_acquire) != 3)
    ;
  // 以下 assert 永远不会失败
  assert(a == 1 && b == 2);
  assert(b.load(memory_order_relaxed) == 2);
}

memory_order_consume:

  1. 对当前要读取的内存施加 release 语义(store),在代码中这条语句后面所有与这块内存有关的读写操作都无法被重排到这个操作之前
  2. 在这个原子变量上施加 release 语义的操作发生之后,consume 可以保证读到所有在 release 前发生的并且与这块内存有关的写入,举个例子:
a = 0;
c = 0;

thread 1:
{
  a = 1;
  c.store(3, memory_order_release);
}

thread 2:
{
  while (c.load(memory_order_consume) != 3)
    ;
  assert(a == 1); // assert 可能失败也可能不失败
}

*memory_order_acq_rel: *

  1. 对读取和写入施加 acquire-release 语义,无法被重排
  2. 可以看见其他线程施加 release 语义的所有写入,同时自己的 release 结束后所有写入对其他施加 acquire 语义的线程可见

memory_order_seq_cst:(顺序一致性)

  1. 如果是读取就是 acquire 语义,如果是写入就是 release 语义,如果是读取+写入就是 acquire-release 语义
  2. 同时会对所有使用此 memory order 的原子操作进行同步,所有线程看到的内存操作的顺序都是一样的,就像单个线程在执行所有线程的指令一样

通常情况下,默认使用 memory_order_seq_cst,所以你如果不确定怎么这些 memory order,就用这个。

以上就是这六种 memory_order 的简单介绍,除此之外还有些重要的概念,比如 sequence-before, happens-before 等等,具体可以参考 std::memory_order - cppreference.com

RocksDB SkipList Memory Order

下面我们结合代码具体看下 RocksDB SkipList 中的 memory order 使用。这部分内容需要你提前熟悉下相关代码。

RocksDB SkipList 支持一写多读。它涉及了三种 memory order,包括 relaxed, release 和 acquire。

一写多读有以下几点限制:

1\. 写入会在外部进行同步
2\. 读取期间 SkipList 不会被销毁
3\. SkipList 节点一旦被插入,不会被删除,除非 SkipList 被销毁
4\. SkipList 节点一旦被插入,除了 next 域会变更外,其他域不会改变

我们把所有涉及 memory order 的操作分为三类:

  1. SkipList 的 max_height_,代表跳跃表的高度。这个值始终使用 relaxed 语义去进行读写,并且只有在插入的时候才可能会改变。因为只有一个写线程,所以对写来说不会读到旧值;对于读,我们一一分析:
1\. 读到旧的 max_height_:不影响查找,我们可能读取到新插入的节点也可能读不到
2\. 读到新的 max_height_:
  a. 读到 head_ 指向的旧节点,那么当我们查找 key 时,会发现 head_ 指向 nullptr,那么会立即下降到下一层
  b. 读到 head_ 指向的新插入节点,那么会使用这个新节点进行查找

2. SkipList 的节点写操作。

for (int i = 0; i < height; i++) {
  x->NoBarrier_SetNext(i, prev_[i]->NoBarrier_Next(i)); // relaxed
  prev_[i]->SetNext(i, x); // release
}

对于正在初始化的节点来说,我们使用 relaxed 语义,即 NoBarrier_SetNext() 和 NoBarrier_Next(),因为这时候节点还没有正式被加入到 SkipList,即对读线程不可见,所以可以使用较弱的 relaxed 语义,但是会在初始化完成后使用 release 语义将节点插入到 SkipList 中,即 SetNext()。根据 release 语义,之前所有 relaxed 操作在这个节点被插入到 SkipList 后对于其他线程的 acquire 操作都是可见的。

注意这里插入节点的整个过程并不是原子的,在每一层插入节点才是原子的。所以有个值得注意的点是在节点插入时我们采用从下到上的方式,因为对于 SkipList 来说,key 在 SkipList 内意味着 key 一定在 level 0,所以如果从上到下插入的话可能出现幻读,即在上层查找比较的时候存在这个 key,但是当下降到 level 0 时发现这个 key 并不存在。

3. SkipList 的节点读操作。对于节点的所有读操作,都会使用 acquire 语义,也就是 Next() 函数,因为要保证我们读取的节点是最新的。

除了顺序插入这个优化,在这个优化里会用 relaxed 语义进行节点读取,也就是 NoBarrier_Next() 函数,因为对于写来说,会有外部同步,所以即使前后两次插入线程不同,使用 relaxed 语义也能读到最新的节点。

PS:

RocksDB SkipList 满足线性一致性,即 Linearizability,如果你了解了线性一致性可以去看下 SkipList 的单元测试。

RocksDB 里面还有一个 SkipList,叫做 InlineSkipList,它是支持多读多写的,节点插入的时候会使用 CAS 判断节点的 next域是否发生了改变,这个 CAS 操作使用默认的memory_order_seq_cst

结语:

Memory Order 是每个底层程序员都需要花时间去掌握的东西,至少会让你对于并发编程的理解会更深。这里有个对于 C++ 11 memory order 的知乎回答, 讲得很简洁明了,知乎用户:如何理解 C++11 的六种 memory order?

然后 RocksDB 也提供了一个很好的学习 memory order 的地方—— SkipList,当初我看代码时直接跳过了原子操作相关的东西,因为感觉很复杂,现在看来花点时间还是能弄明白的。

另外 acquire-release 语义最近也被我放进了自己写的项目里,替代了之前的 full memory barrier,链接就不放出来了。

以下两个官方文档很适合做延伸阅读,尤其是第一个 linux kernal 文档,讲得非常详细,举了很多例子,而且还涉及了很多其他的东西,第二个文档是 C++11 memory order 的 reference。

Linux-Kernal-Memory-Barrier

std::memory_order - cppreference.com

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,047评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,807评论 3 386
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,501评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,839评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,951评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,117评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,188评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,929评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,372评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,679评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,837评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,536评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,168评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,886评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,129评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,665评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,739评论 2 351

推荐阅读更多精彩内容