我的编程空间,编程开发者的网络收藏夹
学习永远不晚

详解c++ atomic原子编程中的Memory Order

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

详解c++ atomic原子编程中的Memory Order

概述

但是,基于内核对象的同步,会带来昂贵的上下文切换(用户态切换到内核态,占用1000个以上的cpu周期)。就需要使用另一种方法 —— 原子指令。

仅靠原子技术实现不了对资源的访问控制,即使简单计数操作,看上去正确的代码也可能会crash。

这里的关键在于编译器和cpu实施的重排指令导致了读写顺序的变化。只要没有依赖,代码中在后面的指令就可能跑到前面去,编译器和CPU都会这么做。

注1:单线程代码不需要关心乱序的问题。因为乱序至少要保证这一原则:不能改变单线程程序的执行行为

注2:内核对象多线程编程在设计的时候都阻止了它们调用点中的乱序(已经隐式包含memory barrier),不需要考虑乱序的问题。

注3:使用用户模式下的线程同步时,乱序的效果才会显露无疑。

程序员可以使用c++11 atomic提供了6种memory order,来在编程语言层面对编译器和cpu实施的重排指令行为进行控制

多线程编程时,通过这些标志位,来读写原子变量,可以组合出4种同步模型:

Relaxed ordering

Release-Acquire ordering

Release-Consume ordering

Sequentially-consistent ordering

默认情况下,std::atomic使用的是Sequentially-consistent ordering(最严格的同步模型)。但在某些场景下,合理使用其它3种ordering,可以让编译器优化生成的代码,从而提高性能。

Relaxed ordering

在这种模型下,std::atomic的load()和store()都要带上memory_order_relaxed参数。Relaxed ordering仅仅保证load()和store()是原子操作,除此之外,不提供任何跨线程的同步。

先看看一个简单的例子:


std::atomic<int> x = 0;     // global variable
std::atomic<int> y = 0;     // global variable
		  
Thread-1:                                  Thread-2:
r1 = y.load(memory_order_relaxed); // A    r2 = x.load(memory_order_relaxed); // C
x.store(r1, memory_order_relaxed); // B    y.store(42, memory_order_relaxed); // D

执行完上面的程序,可能出现r1 == r2 == 42。理解这一点并不难,因为编译器允许调整 C 和 D 的执行顺序。

如果程序的执行顺序是 D -> A -> B -> C,那么就会出现r1 == r2 == 42。

如果某个操作只要求是原子操作,不需要其它同步的保障,就可以使用 Relaxed ordering。程序计数器是一种典型的应用场景。


#include <cassert>
#include <vector>
#include <iostream>
#include <thread>
#include <atomic>
std::atomic<int> cnt = {0};
void f()
{
    for (int n = 0; n < 1000; ++n) {
        cnt.fetch_add(1, std::memory_order_relaxed);
    }
}
int main()
{
    std::vector<std::thread> v;
    for (int n = 0; n < 10; ++n) {
        v.emplace_back(f);
    }
    for (auto& t : v) {
        t.join();
    }
    assert(cnt == 10000);    // never failed
    return 0;
}

Release-Acquire ordering

在这种模型下,store()使用memory_order_release,而load()使用memory_order_acquire。这种模型有两种效果,第一种是可以限制 CPU 指令的重排:

(1)在store()之前的所有读写操作,不允许被移动到这个store()的后面。 // write-release语义

(2)在load()之后的所有读写操作,不允许被移动到这个load()的前面。 // read-acquire语义

该模型可以保证:如果Thread-1的store()的那个值,成功被 Thread-2的load()到了,那么 Thread-1在store()之前对内存的所有写入操作,此时对 Thread-2 来说,都是可见的。

下面的例子阐述了这种模型的原理:


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

让我们分析一下这个过程:

首先 A 不允许被移动到 B 的后面。

同样 D 也不允许被移动到 C 的前面。

当 C 从 while 循环中退出了,说明 C 读取到了 B store()的那个值,此时,Thread-2 保证能够看见 Thread-1 执行 B 之前的所有写入操作(也即是 A)。

使用Release-Acquire ordering实现双重检查锁模式(DLCP)

下面单件为例来说明:


class Singleton
{
public:
    static Singleton* get_instance() {
        Singleton* tmp = instance_.load(std::memory_order_acquire);
        if (tmp == nullptr) {
            std::unique_lock<std::mutex> lk(mutex_);
            tmp = instance_;
            if (tmp == nullptr) {
                tmp = new Singleton();
                instance_.store(std::memory_order_release);
            }
        }
        return tmp;
    }

private:
    Singleton() = default;
    static std::atomic<Singleton*> instance_;
    static std::mutex mutex_;
};

使用Release-Acquire ordering实现自旋锁(Spinlock)

获取和释放语义,是实现锁的基础(Spinlock, Mutex, RWLock, ...),所有被[Read Acquire,Write Release]包含的区域,即构成了一个临界区,临界区里的内存操作,不会乱序到临界区之外执行。

            read-acquire(判断是否加锁,没则加锁,否则循环等待)

-------------------------------------------------------------------------

            all memory operation stay between the line(临界区)

-------------------------------------------------------------------------

                        write-release(释放锁)

实现代码如下:


#include <atomic>
class simple_spin_lock
{
public:
    simple_spin_lock() = default;
    void lock()
    {
        while (flag.test_and_set(std::memory_order_acquire))
            continue;
    }
    void unlock()
    {
        flag.clear(std::memory_order_release);
    }
private:
    simple_spin_lock(const simple_spin_lock&) = delete;
    simple_spin_lock& operator =(const simple_spin_lock&) = delete;
    std::atomic_flag flag = ATOMIC_FLAG_INIT;
};

①对std::atomic_flag的操作具有原子性,保证了同一时间,只有一个线程能够lock成功,其余线程全部在while循环

②使用了acquire内存屏障, 所以lock具有获取语义

③使用了release内存屏障, 所以unlock具有释放语义

Release-Consume ordering

在这种模型下,store()使用memory_order_release,而load()使用memory_order_consume。这种模型有两种效果,第一种是可以限制 CPU 指令的重排:

(1)在store()之前的所有读写操作,不允许被移动到这个store()的后面。

(2)在load()之后的所有依赖此原子变量的读写操作,不允许被移动到这个load()的前面。

注:不依赖此原子变量的读写操作可能会CPU指令重排

下面的例子阐述了这种模型的原理:


#include <thread>
#include <atomic>
#include <cassert>
#include <string>

std::atomic<std::string*> ptr;
int data;
// thread1
void producer()
{
    std::string* p  = new std::string("Hello"); // A
    data = 42; // B
    ptr.store(p, std::memory_order_release); // C
}
// thread2
void consumer()
{
    std::string* p2;
    while (!(p2 = ptr.load(std::memory_order_consume))) // D
        ;
    assert(*p2 == "Hello"); //E     always true: *p2 carries dependency from ptr
    assert(data == 42); // F     may be false: data does not carry dependency from ptr
}

int main()
{
    std::thread t1(producer);
    std::thread t2(consumer);
    t1.join(); 
    t2.join();
    return 0;
}

Sequentially-consistent ordering

所有以memory_order_seq_cst为参数的原子操作(不限于同一个原子变量),对所有线程来说有一个全局顺序(total order)

并且两个相邻memory_order_seq_cst原子操作之间的其他操作(包括非原子变量操作),不能reorder到这两个相邻操作之外

UE4下的Memory Order


enum class EMemoryOrder
{
    // Provides no guarantees that the operation will be ordered relative to any other operation.
    Relaxed,

    // Establishes a single total order of all other atomic operations marked with this.
    SequentiallyConsistent  // Load和Store函数缺省为该类型
};

详见:UnrealEngine\Engine\Source\Runtime\Core\Public\Templates\Atomic.h

Atomic相关的测试代码见:UnrealEngine\Engine\Source\Runtime\Core\Private\Tests\Misc\AtomicTest.cpp

以上就是详解c++ atomic原子编程中的Memory Order的详细内容,更多关于c++ atomic原子编程中的Memory Order的资料请关注编程网其它相关文章!

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

详解c++ atomic原子编程中的Memory Order

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

c++ atomic原子编程中Memory Order的示例分析

这篇文章给大家分享的是有关c++ atomic原子编程中Memory Order的示例分析的内容。小编觉得挺实用的,因此分享给大家做个参考,一起跟随小编过来看看吧。概述但是,基于内核对象的同步,会带来昂贵的上下文切换(用户态切换到内核态,占
2023-06-15

详解C++编程中一元运算符的重载

可重载的一元运算符如下:!(逻辑“非”)&(取址)~(二进制反码)*(取消指针引用)+(一元加)-(一元求反)++(递增)--(递减)转换运算符后缀递增和递减运算符(++ 和 ??)在递增和递减中单独处理,下面会讲到。 以下规则适用于所有其
2022-06-04

详解Python编程中对Monkey Patch猴子补丁开发方式的运用

Monkey patch就是在运行时对已有的代码进行修改,达到hot patch的目的。Eventlet中大量使用了该技巧,以替换标准库中的组件,比如socket。首先来看一下最简单的monkey patch的实现。class Foo(ob
2022-06-04

C++ 函数递归详解:递归在编程竞赛中的应用

递归是一种函数自调用技术,它基于更小的实例解决问题,然后组合结果解决原始问题。其优点包括代码简洁和解决自相似问题的能力,缺点是可能导致堆栈溢出。斐波那契数列等问题可以通过递归函数轻松计算。在编程竞赛中,递归可用于求解迷宫、查找最短路径和排序
C++ 函数递归详解:递归在编程竞赛中的应用
2024-05-04

C++ 函数参数详解:函数式编程中参数传递的思想

c++++ 函数中参数传递有五种方式:引用传递、值传递、隐式类型转换、const 参数、默认参数。引用传递提高效率,值传递更安全;隐式类型转换自动将其他类型转换为函数期望的类型;const 参数防止意外修改;默认参数允许省略某些参数。在函数
C++ 函数参数详解:函数式编程中参数传递的思想
2024-04-28

C++ 成员函数详解:对象方法在异步编程中的作用

成员函数在异步编程中起着至关重要的作用:允许对耗时的任务进行封装,将计算与调用代码分离开来。使应用程序可以在后台执行任务的同时继续运行,提高响应性。创建响应迅速且能利用多核架构的现代 c++++ 应用程序。C++ 成员函数详解:对象方法在异
C++ 成员函数详解:对象方法在异步编程中的作用
2024-04-30

Java网络编程和NIO详解7:浅谈 Linux 中NIO Selector 的实现原理

本文转自互联网本系列文章将整理到我在GitHub上的《Java面试指南》仓库,更多精彩内容请到我的仓库里查看https://github.com/h3pl/Java-Tutorial喜欢的话麻烦点下Star哈文章将同步到我的个人博客:www
2023-06-02

C++ 函数参数详解:泛型编程中参数传递的多态性

泛型编程中 c++++ 函数参数的多态性泛型函数的参数可以采用不同类型(参数传递的多态性),实现针对不同数据类型工作的灵活代码。参数传递方式有三种:值传递:副本传递,不会影响原始参数引用传递:引用传递,反映原始参数的更改指针传递:指针传递,
C++ 函数参数详解:泛型编程中参数传递的多态性
2024-04-26

C++ 函数参数详解:并行编程中参数传递的性能优化

多线程环境中,函数参数传递方式不同,性能差异显著:按值传递:复制参数值,安全,但大型对象开销大。按引用传递:传递引用,效率高,但函数修改会影响调用者。按常量引用传递:传递常量引用,安全,但限制函数对参数操作。按指针传递:传递指针,灵活,但指
C++ 函数参数详解:并行编程中参数传递的性能优化
2024-04-27

编程热搜

  • Python 学习之路 - Python
    一、安装Python34Windows在Python官网(https://www.python.org/downloads/)下载安装包并安装。Python的默认安装路径是:C:\Python34配置环境变量:【右键计算机】--》【属性】-
    Python 学习之路 - Python
  • chatgpt的中文全称是什么
    chatgpt的中文全称是生成型预训练变换模型。ChatGPT是什么ChatGPT是美国人工智能研究实验室OpenAI开发的一种全新聊天机器人模型,它能够通过学习和理解人类的语言来进行对话,还能根据聊天的上下文进行互动,并协助人类完成一系列
    chatgpt的中文全称是什么
  • C/C++中extern函数使用详解
  • C/C++可变参数的使用
    可变参数的使用方法远远不止以下几种,不过在C,C++中使用可变参数时要小心,在使用printf()等函数时传入的参数个数一定不能比前面的格式化字符串中的’%’符号个数少,否则会产生访问越界,运气不好的话还会导致程序崩溃
    C/C++可变参数的使用
  • css样式文件该放在哪里
  • php中数组下标必须是连续的吗
  • Python 3 教程
    Python 3 教程 Python 的 3.0 版本,常被称为 Python 3000,或简称 Py3k。相对于 Python 的早期版本,这是一个较大的升级。为了不带入过多的累赘,Python 3.0 在设计的时候没有考虑向下兼容。 Python
    Python 3 教程
  • Python pip包管理
    一、前言    在Python中, 安装第三方模块是通过 setuptools 这个工具完成的。 Python有两个封装了 setuptools的包管理工具: easy_install  和  pip , 目前官方推荐使用 pip。    
    Python pip包管理
  • ubuntu如何重新编译内核
  • 改善Java代码之慎用java动态编译

目录