美文网首页
原子操作内存序

原子操作内存序

作者: GOGOYAO | 来源:发表于2020-05-05 12:07 被阅读0次

[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线程中进行的,不能保证两者执行的顺序导致。

相关文章

  • 原子操作内存序

    [TOC] 参考 1. C++11多线程-内存模型2. c++并发编程1.内存序3. 浅谈Memory Reord...

  • 2019-01-06 #关于无锁化#

    原子操作 原子操作在操作内存的时候不可以被打断原子读:不会读一半被打断,写了其他值进去原子写:不会因为进线程的调度...

  • 2018-11-11 #C++ 内存模型#

    C++ 原子操作内存模型 C++ 原子操作内存模型解决并发编程的什么问题? 相信在大多数应用程序员眼里,代码在编辑...

  • 2018-05-22

    nonatomic的内存管理语义是非原子性的,非原子性的操作本来就是线程不安全,而atomic的操作是原子性的,但...

  • iOS nonatomic与atomic

    nonatomic的内存管理语义是非原子性的,非原子性的操作本来就是线程不安全,而atomic的操作是原子性的,但...

  • 原子类型与原子操作

    原子类型 c++11提供了原子操作类型, 模板类std::atomic。头文件#include 内存模型 在原子类...

  • Java并发学习之synchronized(一)

    synchronized 具有原子性,可见性。原子性:由java内存模型来直接保证的原子性变量操作包括read,l...

  • java内存模型理解

    java内存模型理解 JVM 内存结构:堆、栈、方法区等等。。 原子性:对基本数据类型的变量和赋值操作才是原子性的...

  • 原子操作、内存屏障、锁

    技术缘由 多核多线程下同时操作相同内存地址产生的竞态问题 CPU结构 乱序执行技术 1.编译器指令重排现代编译器为...

  • Volatile关键字的效果

    今天复习了下java内存模型,原子性,可见性,有序性。以下是概念。 什么是原子性:即一个操作或者多个操作 要么全部...

网友评论

      本文标题:原子操作内存序

      本文链接:https://www.haomeiwen.com/subject/scixqhtx.html