C++11多线程 内存序(std::memory_order_relaxed)

目录

引言

cpu架构

std::memory_order_relaxed(宽松内存序)介绍

示例代码


写在最前面的话

本人在某厂infra做C++相关开发,也会时常同C++并发编程打交道,因此决定将C++并发编程相关知识点记录成博客。本系列主要根据C++多线程并发实践这本书,分享相应的多线程编程的知识。

由于最近发现我辛苦写的文章被copy缺没有写上引用我这篇文章,导致我有点不舒服。所以我决定把这个专栏收费了。

就这样吧。我继续去出博客了,好久没更新了 希望在新的一年能更新一篇,提前祝大家2023年快乐。

欢迎关注我的公众号:松元漫话

引言

本文是讲解C++内存序一列文章中的一部分,主要讲解宽松内存序的理解和使用。

本部分将从如下三方面讲解:

  1. 一个粗略的可能存在的现代cpu架构
  2. 宽松内存序介绍
  3. 示例代码(主要来自《C++ Concurrency in Action》)

关于内存模型相关知识可参考C++多线程 内存序(顺序一致性)

cpu架构

一个粗略的现代cpu架构可能如下所示

上述提供了一个粗略的现代CPU架构,上述中CPU标注的块,代表着一个Core,此处说明一下。

在上述4core系统中,每两个core构成一个bank,并共享一个cache,且每个core均有一个store buffer。

本文给出的多核结构仅仅为了理解std::memory_order_relaxed作为基础,所以有些解释非官方,譬如我们假设,每个CPU所作的store均会写到store buffer中,关于何时写入到cache甚至memory不再我们的考虑范围之内,只需要知道每个CPU会在任何时刻将store buffer中结果写入到cache或者memory中。

关于cache之间的一致性协议不再本文讲述范围内,感兴趣的可参考:Memory_Barriers_a_Hardware_View_for_Software_Hackers

std::memory_order_relaxed(宽松内存序)介绍

关于std::memory_order_relaxed具备如下几个功能:

  1. 作用于原子变量
  2. 不具有synchronizes-with关系
  3. 对于同一个原子变量,在同一个线程中具有happens-before关系(言外之意,不同的原子变量不具有happens-before关系,可以乱序执行)
  4.  由3可知,在一个线程中,如果某个表达式已经看到原子变量的某个值a,则该表达式的后续表达式只能看到a或者比a更新的值

示例代码

如下代码摘抄至《C++ Concurrency in Action》

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

std::atomic<bool> x, y;
std::atomic<int> z;

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
        /* code */
    }

    if (x.load(std::memory_order_relaxed)) { //4
        ++z;
    }
    
}

int main(int argc, char* argv[]) {

    x = false;
    y = false;
    z = 0;

    std::thread t1(write_x_then_y);
    std::thread t2(read_y_then_x);
    t1.join();
    t2.join();
    assert(z.load() != 0); // 5
    return 0;
}

上述代码,在x86架构的机器上跑,永远不会触发 5,但是若是ARM架构,则可能触发5。

关于该代码的理解,我们可以结合上述CPU架构小节来理解,假设线程t1运行在CPU1,线程t2运行在CPU3,std::memory_order_relaxed在此处可以理解为仅仅保持原子性,没有其他的作用。因此线程1虽然更新x,y为true,但由于无法保证 两者都同时对其他CPU可见(每个CPU可能在任何时刻将其store buffer中的值写入cache或者memory,此时才有机会被其他CPU看见)。

因此上述可能存在如下执行顺序:

  1.  标记1执行,x为true
  2. 标记2执行,y为true
  3. CPU1将y写入cache或者memory,CPU3可以看见改值
  4. 标记3执行,y为true
  5. 标记4执行,cache中的x为false,z为0
  6. 标记5执行,触发断言

当然你也可以用以下执行顺序理解上述代码(由于std::memory_order_relaxed在不同变量间不具有happens-before关系,因此,标记2可以在标记1之前执行), 故也可能存在如下执行顺序:

  1. 标记2执行,y为true
  2. 标记3执行,y为true
  3. 标记4执行,x为false,z为0
  4. 标记1执行,x为true
  5. 标记5执行,触发断言

针对该种执行顺序,书中给出了如下示意图,相信目前你也能理解该图含义

再来看一段更复杂的例子,其同样来源于《C++ Concurrency in Action》

#include <thread>
#include <atomic>
#include <iostream>
#include <cassert>

std::atomic<int> x(0), y(0), z(0);
std::atomic<bool> go;
unsigned const int loop_num = 10;

struct read_values {
    int x, y, z;
}

// 定义五个read_values数组
read_values values1[loop_num];
read_values values2[loop_num];
read_values values3[loop_num];
read_values values4[loop_num];
read_values values5[loop_num];

void increment(std::atomic<int>* var_to_inc, read_values* values) {
    // 利用go标记启动线程,以保证所有线程能同时启动
    while (!go) {
        // 让出CPU给其他线程
        std::this_thread::yield();
    }
    
    // 针对某个原子变量,在每次循环时将其赋值为i+1,并在下次load时读取
    for (unsigned i = 0; i < loop_num; ++i) {
        values[i].x = x.load(std::memory_order_relaxed);
        values[i].y = y.load(std::memory_order_relaxed);
        values[i].z = z.load(std::memory_order_relaxed);
        var_to_inc->store(i+1, std::memory_order_relaxed);
        std::this_thread::yield();
    }
    return;
}

void read_vals(read_values* values) {
    while (!go) {
        std::this_thread::yield();
    }
    for (unsigned i = 0; i < loop_num; ++i) {
        values[i].x = x.load(std::memory_order_relaxed);
        values[i].y = y.load(std::memory_order_relaxed);
        values[i].z = z.load(std::memory_order_relaxed);
        std::this_thread::yield();
    }
}

void print(read_values* v) {
    for (unsigned i = 0; i < loop_num; ++i) {
        if (i) {
            std::cout << ", ";
        }

        std::cout<<"("<<v[i].x<<","<<v[i].y<<","<<v[i].z<<")";
    }
    std::cout<<std::endl;
}

int main(int argc, char* argv[]) {

    std::thread t1(increment,&x,values1);
    std::thread t2(increment,&y,values2);
    std::thread t3(increment,&z,values3);
    std::thread t4(read_vals,values4);
    std::thread t5(read_vals,values5);
    go=true;
    t5.join();
    t4.join();
    t3.join();
    t2.join();
    t1.join();
    print(values1);
    print(values2);
    print(values3);
    print(values4);
    print(values5);
    return 0;
}

上述代码一个可能结果如下:

(0,0,0),(1,0,0),(2,0,0),(3,0,0),(4,0,0),(5,7,0),(6,7,8),(7,9,8),(8,9,8),
(9,9,10)
(0,0,0),(0,1,0),(0,2,0),(1,3,5),(8,4,5),(8,5,5),(8,6,6),(8,7,9),(10,8,9),
(10,9,10)
(0,0,0),(0,0,1),(0,0,2),(0,0,3),(0,0,4),(0,0,5),(0,0,6),(0,0,7),(0,0,8),
(0,0,9)
(1,3,0),(2,3,0),(2,4,1),(3,6,4),(3,9,5),(5,10,6),(5,10,8),(5,10,10),
(9,10,10),(10,10,10)
(0,0,0),(0,0,0),(0,0,0),(6,3,7),(6,5,7),(7,7,7),(7,8,7),(8,8,7),(8,8,9),
(8,8,9)

理解为什么会出现该结果依然需要结合CPU架构小节的内容,std::memory_order_relaxed内存序针对同一个原子变量,在同一个线程具有happens-before关系, 因此若在同一个线程中,先store一个值,则后续load必然会看到这个store的值,因此values1,values2,values3的输出中,x,y,z均是单调递增的。

同上述讲解一样,对于不同的线程,std::memory_order_relaxed内存序不保证读取值的同步,但若同一个线程已经读取到某个值a,则后续的load不能读取到比a更老的值。

因此便会出现上述结果!

增加:

欢迎大家关注公众号互相交流(松元漫话

对于本系列,我进行了完善,并整理了20篇文章,涉及C语言并发,C++并发,缓存一致性协议,C++20相关的并发特性等,需要的可以私信我,19.9一份。大家一定要先私信我再付钱。

  • 20
    点赞
  • 61
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
std::atomic_thread_fence是C++中的一个原子操作函数,用于创建一个内存屏障,确保在调用该函数之前和之后的所有内存访问都按照顺来执行。这个函数可以在多线程编程中用于确保可见性和顺性的要求。 内存屏障是一种同步原语,用于控制内存操作的执行顺和可见性。在多线程环境中,由于指令重排和缓存一致性等因素的存在,不同线程对共享变量的读写操作可能会出现不一致的情况。通过使用内存屏障,我们可以显式地指定一些内存操作的执行顺,以避免这种问题。 std::atomic_thread_fence函数有几个重载形式,可以指定不同的内存要求。它可以接受一个memory_order参数,用于指定内存访问的顺保证。常见的memory_order选项有: - memory_order_relaxed:松散顺,不对任何指针访问进行顺约束。 - memory_order_acquire:获取顺,确保当前线程对共享变量的读操作在本条原子操作之前完成。 - memory_order_release:释放顺,确保当前线程对共享变量的写操作在本条原子操作之后完成。 - memory_order_acq_rel:获取-释放顺,结合了acquire和release的特性,既确保读操作在之前完成,也确保写操作在之后完成。 - memory_order_seq_cst:顺一致性,对所有线程的内存访问进行全局排,保证各线程间的操作顺一致。 这些memory_order选项可以灵活地根据具体的需求来选择,以实现所需的同步和顺性要求。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

qls315

感觉好可打赏几毛钱增强更新动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值