原子操作内存序

[TOC]

参考

1. C++11多线程-内存模型
2. c++并发编程1.内存序
3. 浅谈Memory Reordering
4. C++11中的内存模型下篇 - C++11支持的几种内存模型
5. C++11中的内存模型上篇 - 内存模型基础

前言

有三种情况,可能导致乱序执行:编译器优化、CPU乱序、缓存不一致。进而导致多线程情况下出现问题。[1,3,4]

c++11引入了atomic类型之后,大大方便了原子变量的使用,但是原子变量的内存序有好几种,这又引入了让人难以理解的内容。
内存序分为三类六种

  • relaxed(松弛的内存序)
  • sequential_consistency(内存一致序)
  • acquire-release(获取-释放一致性)

relaxed

//test.cpp
#include <thread>
#include <atomic>
#include <assert.h>

std::atomic<bool> x{false},y{false};
std::atomic<int> z{0};

void write_x_then_y() {
    x.store(true,std::memory_order_relaxed);   //1
    y.store(true,std::memory_order_relaxed);   //2
}

void read_y_then_x() {
    while(!y.load(std::memory_order_relaxed));  //3
    if(x.load(std::memory_order_relaxed))     //4
        ++z;
}

int main() {
    std::thread b(read_y_then_x);
    std::thread a(write_x_then_y);
    a.join();
    b.join();
    if (z.load() != 0) return 0; else return 1;
}
# test.sh
#!/bin/bash
for ((i=0;i<1;)); do
    ./a.out
    if [ "$?" == "1" ];then
        break
    fi
done

g++ -std=c++17 -pthread -O2 test.cpp编译以上代码,time sh test.sh执行代码。

如果出现 2 -> 3 -> 4 -> 1这样的执行次序,那么就会出现z == 0这种错误情况。

<font color=red>注,不过我跑了一晚上,并没有复现这个结果</font>

那么relaxed用于何处呢?对于计数这种场景,就可以使用relaxed来最大化性能。

#include <cassert>
#include <vector>
#include <thread>
#include <atomic>

std::atomic<int> count{0};
void f() {
    for (int n = 0; n < 1000; ++n) {
        count.fetch_add(1, std::memory_order_relaxed);
    }
}
int main() {
    std::thread threads[10];
    for (std::thread &thr: threads) {
        thr = std::thread(f);
    }
    for (auto &thr : v) {
        thr.join();
    }
    assert(cnt == 10000); // 永远不会失败
    return 0;
}

release-acquire

针对relaxed的例子,如果改成如下的代码就可以避免z == 0这种错误情况。

#include <thread>
#include <atomic>
#include <assert.h>

std::atomic<bool> x{false},y{false};
std::atomic<int> z{0};

void write_x_then_y() {
    x.store(true,std::memory_order_relaxed);   //1
    y.store(true,std::memory_order_release);   //2
}

void read_y_then_x() {
    while(!y.load(std::memory_order_acquire));  //3
    if(x.load(std::memory_order_relaxed))     //4
        ++z;
}

int main() {
    std::thread b(read_y_then_x);
    std::thread a(write_x_then_y);
    a.join();
    b.join();
    if (z.load() != 0) return 0; else return 1;
}

他会保证1发生在2前,4发生在3后,同时3一定发生在2后,那么z == 0不会发生。

如下图分析

image.png
  • 初始条件为x = y = false。
  • 在write_x_then_y线程中,先执行对x的写操作,再执行对y的写操作,由于两者在同一个线程中,所以即便针对x的修改操作使用relaxed模型,修改x也一定在修改y之前执行。
  • 在write_x_then_y线程中,对y的load操作使用了acquire模型,而在线程write_x_then_y中针对变量y的读操作使用release模型,因此保证了是先执行write_x_then_y函数才到read_y_then_x的针对变量y的load操作。
  • 因此最终的执行顺序如上图所示,此时不可能出现z=0的情况。

从以上的分析可以看出,针对同一个变量的release-acquire操作,更多时候扮演了一种“线程间使用某一变量的同步”作用,由于有了这个语义的保证,做到了线程间操作的先后顺序保证(inter-thread happens-before)。

可以简单记作release为写不后,acquire为读不前。[2]

release-consume

官方不推荐,此处不进行详细描述。简单说,release-acquire会把不相关的变量存取都进行保序,release-consume只会对有依赖的变量保序,进而提高效率,同时也使代码更容易引入bug。

sequential consistency

这是最严格的级别,也是性能最差的级别,同时也是默认的级别。
如下列:

#include <thread>
#include <atomic>
#include <cassert>
 
std::atomic<bool> x = {false};
std::atomic<bool> y = {false};
std::atomic<int> z = {0};
 
void write_x() {
    x.store(true, std::memory_order_seq_cst);  // 1
}
 
void write_y() {
    y.store(true, std::memory_order_seq_cst);  // 2
}
 
void read_x_then_y() {
    while (!x.load(std::memory_order_seq_cst));  // 3
    if (y.load(std::memory_order_seq_cst)) {  // 4
        ++z;
    }
}
 
void read_y_then_x() {
    while (!y.load(std::memory_order_seq_cst));  // 5
    if (x.load(std::memory_order_seq_cst)) {   // 6
        ++z;
    }
}
 
int main() {
    std::thread a(write_x);
    std::thread b(write_y);
    std::thread c(read_x_then_y); // thread c
    std::thread d(read_y_then_x); // thread d
    a.join(); b.join(); c.join(); d.join();
    // failed to assert without memory_order_seq_cst
    assert(z.load() != 0);
}

如果使用release-acquire,那么线程c可能看到的是2 -> 1这个执行顺序,但是线程d可能看到的是1->2这个执行顺序,进而导致z == 0

如下图分析:


image.png
  • 初始条件为x = y = false。
  • 由于在read_x_and_y线程中,对x的load操作使用了acquire模型,因此保证了是先执行write_x函数才到这一步的;同理先执行write_y才到read_y_and_x中针对y的load操作。
  • 然而即便如此,也可能出现在read_x_then_y中针对y的load操作在y的store操作之前完成,因为y.store操作与此之间没有先后顺序关系;同理也不能保证x一定读到true值,因此到程序结束是就出现了z = 0的情况。

从上面的分析可以看到,即便在这里使用了release-acquire模型,仍然没有保证z==0,其原因在于:最开始针对x、y两个变量的写操作是分别在write_x和write_y线程中进行的,不能保证两者执行的顺序导致。

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