当前位置: 首页 > article >正文

【一分钟学C++】std::memory_order

在这里插入图片描述

竹杖芒鞋轻胜马,谁怕?一蓑烟雨任平生~
公众号: C++学习与探索  |  个人主页: rainInSunny  |  个人专栏: Learn OpenGL In Qt

文章目录

  • 写在前面
  • 为什么需要Memory Order
  • Memory Order
    • Relaxed Order
    • Release-Acquire Order

写在前面

  使用std::memory_order是用来限制编译器以及CPU对单线程当中的指令执行顺序进行重排的程度。这种限制,决定了以atom操作为基准点,对其之前的内存访问命令,以及之后的内存访问命令,能够在多大的范围内自由重排,从而形成了6种模式。这里我们主要讨论std::memory_order_relaxedstd::memory_order_acquirestd::memory_order_release。 注意,std::memory_order限制的是单线程中的CPU指令乱序,但一般用来解决多线程同步的问题。

为什么需要Memory Order

  如果不使用任何同步机制(例如mutex或atomic),在多线程中读写同一个变量,程序的结果是难以预料的。简单来说,编译器以及CPU的一些行为,会影响到程序的执行结果:

  • 即使是简单的语句,C++也不保证是原子操作。
  • CPU可能会调整指令的执行顺序。
  • 在CPU cache的影响下,一个CPU执行了某个指令,不会立即被其它CPU看见。
// 场景1:C++不保证线程2输出的是100,因为i = 100不是原子操作,可能存在中间态
int i = 0;
Thread_1:
i = 100;

Thread_2:
std::cout << i;

// 场景2:CPU可能会调整指令的执行顺序,这里假设所有操作都是原子操作,仍然可能输出0或者100
int x = 0;
int y = 0;

Thread_1:
x = 100;
y = 200;

Thread_2:
while (y != 200)
    ;
std::cout << x;

// 场景3:假设A先于B,但CPU cache的影响下,Thread_2不能保证立即看到A操作的结果,所以Thread_2可能输出0或100
int x = 0;

Thread_1:
x = 100; // A

Thread_2:
std::cout << x; // B

  场景1,i = 100;不是原子操作导致了结果不确定;场景2,CPU会在不影响当前线程执行逻辑情况下对指令执行顺序进行优化,如果Thread_1将y = 200调整到x = 100之前执行,那么可能输出0或者100;场景3,由于CPU缓存,可能导致Thread_2在输出的时候Thread_1中x的值还不可见,导致可能输出0或者100。

typedef enum memory_order {
    memory_order_relaxed, // relaxed
    memory_order_consume, // consume
    memory_order_acquire, // acquire
    memory_order_release, // release
    memory_order_acq_rel, // acquire/release
    memory_order_seq_cst  // sequentially consistent
} memory_order;

  可以看出多线程读写变量需要同步机制,常见的有std::mutexstd::atomic,对比两者std::atomic性能要更好一些。C++标准库提供了std::memory_orderstd::atomic一起实现多线程之间同步,下面主要讲解std::memory_order_relaxedstd::memory_order_acquirestd::memory_order_release

Memory Order

Relaxed Order

std::atomic<int> x = 0;
std::atomic<int> y = 0;

Thread_1:
r1 = y.load(std::memory_order_relaxed); // A
x.store(r1, std::memory_order_relaxed); // B

Thread_2:
r2 = x.load(memory_order_relaxed); // C
y.store(66, memory_order_relaxed); // D

  执行完上面的程序,可能出现r1 == r2 == 66。理解这一点并不难,因为编译器允许调整C和D的执行顺序。如果程序的执行顺序是D -> A -> B -> C,那么就会出现r1 == r2 == 66。当只需要保证原子性,不需要其它同步操作时,选择使用std::memory_order_relaxed

Release-Acquire Order

  在这种模型下,store()使用std::memory_order_release,而load()使用std::memory_order_acquire。这种模型有两种效果,第一种是可以限制CPU指令的重排。除此之外,还有另一种效果:假设 Thread_1中store()的那个值成功被Thread_2中load()到了,那么Thread_1在store()之前对内存的所有写入操作,此时对Thread_2来说都是可见的。

  • 在store()之前的所有内存读写操作,不允许被移动到这个store()的后面。
  • 在load()之后的所有内存读写操作,不允许被移动到这个load()的前面。
#include <thread>
#include <atomic>
#include <cassert>
#include <string>

std::atomic<bool> ready{ false };
int data = 0;
void producer()
{
    data = 66; // A
    ready.store(true, std::memory_order_release); // B
}
void consumer()
{
    while (!ready.load(std::memory_order_acquire)) // C
        ;
    assert(data == 66); // D,never failed
}
int main()
{
    std::thread t1(producer);
    std::thread t2(consumer);
    t1.join();
    t2.join();
    return 0;
}

  由于在store()之前的所有内存读写操作,不允许被移动到这个store()的后面,所以线程t1中A操作一定在B操作之前就执行完毕了。线程t2中C操作跳出循环时意味着线程t1中B操作执行完毕,那么data此时肯定已经被赋值为66,所以assert永远不会失败。反之如果这里使用std::memory_order_relaxed,那么t1线程中data的赋值可能被CPU调整到store()后面,那就可能导致assert失败。

在这里插入图片描述


关注公众号:C++学习与探索,有惊喜哦~


http://www.kler.cn/a/304175.html

相关文章:

  • SwiftUI开发教程系列 - 第1章:简介与环境配置
  • C++初阶——vector
  • 面试题之---解释一下原型和原型链
  • 如何选择最适合的自闭症干预机构?
  • vue2项目启用tailwindcss - 开启class=“w-[190px] mr-[20px]“ - 修复tailwindcss无效的问题
  • Java NIO实现高性能HTTP代理
  • Vue3+Django5+REST Framework开发电脑管理系统
  • 【计算机网络 - 基础问题】每日 3 题(一)
  • 程序的结构和控制流与数据流
  • MySQL 表的增删改查
  • 注解(Java程序的一种特殊“注释”,用于工具处理的标注)
  • 每日一问:C++ 中重写和重载的区别
  • vue3 5个常用的API
  • SpringBoot开发——整合Spring Data MongoDB
  • [数据集][目标检测]车油口挡板开关闭合检测数据集VOC+YOLO格式138张2类别
  • 凸优化学习(2)——梯度类方法求解(gradient descent)
  • 构建有温度的用户关系:开源 AI 智能名片、链动 2+1 模式与 S2B2C 商城小程序的作用
  • 华为SMU02B1管理模块WEB登录与账户密码信息
  • HTB-Archetype(winPEAS枚举工具,mssql xp_cmdshell)
  • Linux - make/Makefile工具的基础使用
  • Java的发展史与前景
  • 贪吃蛇项目实现(C语言)——附源码
  • JavaScript知识点3
  • JMeter脚本开发
  • 人工智能领域的性能指的是什么
  • Unity3D类似于桌面精灵的功能实现