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架构可能如下所示

C++11多线程 内存序(std::memory_order_relaxed)_第1张图片

上述提供了一个粗略的现代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 
#include 
#include 

std::atomic x, y;
std::atomic 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++11多线程 内存序(std::memory_order_relaxed)_第2张图片

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

#include 
#include 
#include 
#include 

std::atomic x(0), y(0), z(0);
std::atomic 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* 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<<"("<

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

(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更老的值。

因此便会出现上述结果!

增加:

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

你可能感兴趣的:(C++多线程,c++,算法)