03 重修C++之并发实战5

本文详细介绍了C++中的并发实战,重点讲解了内存模型的基础概念,包括对象、内存位置和并发中的数据竞争。同时,深入探讨了C++中的原子操作,如std::atomic_flag和std::atomic<bool>,以及它们在自旋锁和同步操作中的应用。此外,还提到了不同内存顺序模型,如顺序一致、获得-释放和松散顺序,及其在多线程中的作用。文章以实例展示了如何使用原子操作避免数据竞争,确保并发安全。
摘要由CSDN通过智能技术生成

03 重修C++之并发实战5

上一篇:03 重修C++之并发实战4

C++11中的一个重要特性之一就是新的多线程感知内存模型。C++为了提供足够灵活的使用方法,不需要再使用一个比C++更低级的语言,就要允许C++更加接近机器。原子类型和操作正是要允许这一点,提供可通常减至一或两个CPU指令的低阶同步操作的功能。

5.1 内存模型基础

内存模型包括两个方面:

基本结构方面:数据是如何放置在内存中的。

并发方面:结构方面对于并发是很重要的,尤其是低级原子操作中来看。

在C++中,一切都是关于对象和内存位置。

5.1.1 对象和内存位置

C++中所有的数据均是由对象**(object)**组成的。这并不是说你可以创建一个派生自int(基本类型)的新类,也不是说基本类型具有成员函数。C++标准中定义对象为“存储区域”,尽管它会为这些对象分配属性(如类型和生命周期)。无论什么类型,对象均被存储于一个或多个内存位置中。每个这样的内存位置要么是一个标量类型的对象(或子对象),要么是相邻位域的序列。如果使用位域,有非常重要的一点必须注意:虽然相邻位域是不同的对象,但是它们仍然算作相同的内存位置。

  • 每个变量都是一个对象,包括其他对象的成员。
  • 每个对象占据至少一个内存位置。
  • 如int或char这样的基本类型的变量恰好一个内存位置,不论其大小,即便它们相邻或是数组的一部分。
  • 相邻的位域是相同内存的一部分。

注:所谓“位域”是把一个字节中的二进位划分为几 个不同的区域,并说明每个区域的位数。每个域有一个域名,允许在程序中按域名进行操作。 这样就可以把几个不同的对象用一个字节的二进制位域来表示。位段成员必须声明为int、unsigned int或signed int类型(short char long)。

5.1.2 对象、内存位置以及并发

如果两个线程访问不同的内存位置,是没有问题的,但是一旦访问相同内存位置,那么就极有可能造成竞争。为了避免竞争条件,在这两个线程的访问中间,就必须有一个强制的顺序。确保有一个确定的顺序的方法之一就是使用互斥元。另一种方法就是在相同或者是其他的内存位置使用**原子(atomic)**操作 ,在两个线程的访问之间强加一个顺序。如果多于两个线程访问一个内存位置,则每一对访问都必须具有明确的顺序。

如果来自独立线程的两个对同一内存位置的访问没有强制顺序,其中一个或两个访问不是原子的,且一个或两个是写操作,那么这就是数据竞争并导致未定义行为。所以我们可以使用原子操作来访问具有竞争的内存位置,来避免不确定行为。但是这并不阻止竞争本身——原子操作所接触的内存位置首先仍是未指定的,但是却能够将程序带回确定行为的领域。

5.1.3 修改顺序

C++程序中每个对象都具有一个确定的修改顺序(modification order)。它是有来自程序中的所有线程对该对象的所有写入组成的,由对象的初始化开始。在多数情况下,该顺序在每次运行之间都有所不同,但是在任意给定的程序执行里,系统中的所有线程必须一致统一此顺序。如果不使用原子类型,开发者就得负责确认有足够多的同步来确保线程一致同意每个变量的修改顺序。如果不同的线程看到的是一个变量的不同顺序值,就会有数据竞争和未定义行为。如果使用原子操作,编译器将负责确保这些同步已就位。

5.2 C++中的原子操作及类型

5.2.1 标准原子类型

标准原子类型都定义在<atomic>头文件中,标准原子类型几乎都具有一个is_lock_free()成员函数,让用户决定在给定类型上的操作是否直接用原子指令完成(x.is_lock_free()返回true),或是通过使用编译器和类库内部的锁来完成(x.is_lock_free()返回false)。唯一不提供is_lock_free()成员函数的类型是std::atomic_flag。该类型是一个非常简单的布尔标识,并且这个类型上的操作要求是无锁的。

其余的原子类型全部都是通过std::atomic<>类模板特化来访问的,并且有更加完备的功能,但可能不是无锁的。在大多数平台上,我们认为内置类型的原子变种(例如:std::atomic<int>std::atomic<void*>)确实是无锁的,但却并非是要求的。

除了直接使用std::atomic<>,还可以使用下表中的一组命名,这些替代类型可能指相应的std::atomic<>特化,也可能是该特化的基类,而且在一个程序中混用可能会导致不可移植。

原子类型对应特化
atomic_boolstd::atomic<bool>
atomic_charstd::atomic<char>
atomic_scharstd::atomic<signed char>
atomic_ucharstd::atomic<unsigned char>
atomic_intstd::atomic<int>
atomic_uintstd::atomic<unsigned>
atomic_shortstd::atomic<short>
atomic_ushortstd::atomic<unsigned short>
atomic_longstd::atomic<long>
atomic_ulongstd::atomic<unsigned long>
atomic_llongstd::atomic<long long>
atomic_char16_tstd::atomic<char16_t>
atomic_char32_tstd::atomic<char32_t>
atomic_wchar_tstd::atomic<wchar_t>

同样C++标准库也为原子类型提供了一组typedef,对应像std::size_t这样的非原子的标准库typedef。(大多数都是加上atomic_前缀)。

传统意义上,标准原子类型是不可复制且不可赋值的,因为它们没有拷贝构造函数和拷贝赋值运算符。但是它们确实支持从相应的内置类型的赋值并进行隐式转换赋值,与直接load() store()成员函数、exchange() compare_exchange_weak() 以及 compare_exchange_strong() 一样。它们在适当的地方还支持复合赋值运算符:+=、-=、*=、|=等,针对整型和指针的特化,还支持++、–。

然而,std::atomic<>类模板不仅仅是一组特化,还是一个主模板。可以用来创建一个用户定义类型的原子变种。由于是一个泛型类模板,操作只限为load() store()(与用户定义的类型之间相互赋值)、exchange() compare_exchange_weak() 以及 compare_exchange_strong() 。下面将说明一下运算的三种类型的可选顺序:

  • **存储(store)**操作:可以包括memory_order_relaxed、memory_order_release 或 memory_order_seq_cst顺序。
  • **载入(load)**操作:可以包括memory_order_relaxed、memory_order_consume、memory_order_acquire 或 memory_order_seq_cst顺序。
  • **读-修改-写(read-modify-write)**操作,可以包括memory_order_relaxed、memory_order_consume、memory_order_acquire、memory_order_release 、memory_order_acq_rel 或 memory_order_seq_cst顺序。

所有操作的默认顺序为memory_order_seq_cst。

5.2.2 std::atomic_flag 上的操作

std::atomic_flag是最简单的标准原子类型,它代表一个布尔标志。这一个类型的对象可以是两种状态之一:设置或清除。此类型的对象必须使用ATOMIC_FLAG_INIT初始化()初始化为清楚状态,将会在下面的代码中看到。这是唯一需要针对初始化进行特殊处理的原子类型,同时也是保证唯一无锁的类型。该类型的对象具有状态存储持续时间,那么就保证了静态初始化,这意味着不存在初始化顺序的问题,它总是在该标识的首次操作时进行初始化。

一旦标识被初始化完成,就只能对它做三件事:销毁、清除或设置,下面将会看到一个使用的示例。

  • 使用std::atomic_flag实现自旋锁
#include <iostream>
#include <atomic>
#include <thread>
#include <mutex>

class spinlock_mutex
{
    std::atomic_flag flag;
public:
    //构造函数 初始化
    spinlock_mutex():flag(ATOMIC_FLAG_INIT) { } 
	//自旋锁 锁定
    void lock()
    {   //尝试设置 非阻塞的 这里循环调用会造成一定程度上的忙等
        while (flag.test_and_set(std::memory_order_acquire));
        //test_and_set是一个读修改写操作需要 std::memory_order_acquire 顺序
        //或默认不加参数为 memory_order_seq_cst
    }
	//解锁
    void unlock()
    {
        //清楚标志 clear是一个存储操作 不能有 std::memory_order_acquire
        //或 memory_order_acq_rel顺序,可以给一个memory_order_release 或默认顺序。
        flag.clear(std::memory_order_release);
    }    
};
//测试函数主体
int main(int argc, const char** argv) {
    spinlock_mutex m;
    std::thread t1([&]{
        m.lock();
        std::cout << "---------------------t1 get the mutex.------------------\n" << std::endl;
        std::this_thread::sleep_for(std::chrono::milliseconds(5000));
        m.unlock();
        std::cout << "+++++++++++++++t1 sleep 5s and release mutex++++++++++++\n" << std::endl;
    });

    std::thread t2([&]{
        std::this_thread::sleep_for(std::chrono::milliseconds(1000));
        std::cout << "t2 try get mutex----------------------------------------\n" << std::endl;
        m.lock();
        std::cout << "---------------------t2 get the mutex.------------------\n" << std::endl;
        std::this_thread::sleep_for(std::chrono::milliseconds(3000));
        m.unlock();
        std::cout << "+++++++++++++++t2 sleep 3s and release mutex++++++++++++\n" << std::endl;
    });

    t1.join();
    t2.join();
    return 0;
}

/*************************************************************
运行结果:
---------------------t1 get the mutex.------------------

t2 try get mutex----------------------------------------

+++++++++++++++t1 sleep 5s and release mutex++++++++++++

---------------------t2 get the mutex.------------------

+++++++++++++++t2 sleep 3s and release mutex++++++++++++

**************************************************************/

但是这个类型最大的问题是不提供无修改的查询操作,所以大度情况下不能当作通用的布尔标识,而且在某些情况下会造成严重的忙等,这都是不合理的,所以不建议使用。

5.2.3 基于 std::atomic<bool> 的操作

std::atomic<bool>是一个比std::atomic_flag功能更全的布尔标志。虽然它仍是不可拷贝构造和拷贝赋值的,但是可以从一个非原子的bool来构造它,同时也可以从一个非原子的bool值来对std::atomic<bool>的实例赋值。下面提供几种对布尔原子类型的操作:

load:无修改查询。

store:设置状态T或者F。

exchange:读-修改-写,写入新状态并返回旧状态。

下面将用一段程序演示用法:

#include <sstream>
#include <locale>
#include <iostream>
#include <atomic>
#include <thread>
#include <mutex>

int main(int argc, const char** argv) {
	//bool原子类型
    std::atomic<bool> flag;
    //线程t1
    std::thread t1([&]{
        flag = false; //从一个非原子的bool来构造
        std::cout << "t1 set flag = FALSE       by = operator.\n";
        std::this_thread::sleep_for(std::chrono::milliseconds(3000));
        flag.store(true); //用store()设置值
        std::cout << "t1 set flag = TRUE        by store().\n";
        std::this_thread::sleep_for(std::chrono::milliseconds(3000));
        //用exchange()设置值,并保留旧值
        bool old_flag = flag.exchange(false, std::memory_order_acq_rel);
        std::cout << "t1 set flag = FALSE       by exchange().\n";
        std::cout << "t1::old_flag = " << std::boolalpha << old_flag << std::endl;

    });

    std::thread t2([&]{
        std::this_thread::sleep_for(std::chrono::milliseconds(1000));
        for (size_t i = 0; i < 5; i++)
        {	//使用load无修改查询
            if (flag.load(std::memory_order_acquire))
            {
                std::cout << "t2 read flag is TURE  by load().\n";
            }
            else
            {
                std::cout << "t2 read flag is FALSE by load().\n";
            }
            std::this_thread::sleep_for(std::chrono::milliseconds(3000));
        }  

    });

    t1.join();
    t2.join();
    return 0;
}

/************************************************
运行结果:
t1 set flag = FALSE       by = operator.
t2 read flag is FALSE by load().
t1 set flag = TRUE        by store().
t2 read flag is TURE  by load().
t1 set flag = FALSE       by exchange().
t1::old_flag = true
t2 read flag is FALSE by load().
t2 read flag is FALSE by load().
t2 read flag is FALSE by load().
************************************************/

除上述基本功能之外,还有一个操作叫做**“比较/交换”**,它以compare_exchange_weak()和compare_exchange_strong()成员函数的形式出现。

用法:比较原子变量和所提供的期望值(第一个参数),如果两者相等,则将原子类型对象更新为期望值(第二个参数),并返回ture;如果不相等,则期望值(第一个参数)更新为原子变量的实际值,并返回false。举个简单的例子

bool flag;
int exp =0;
std::atomic<int> data;
data = 0;
//比较更新 data == exp 所以data会被更新成2, flag置true
flag = data.compare_exchange_strong(exp, 2);
//结果 data=2, exp=0, flag=true
//------------------------------------------------------------
bool flag;
int exp =1;
std::atomic<int> data;
data = 0;
//比较更新 data != exp 所以exp会被更新成0, flag置false
flag = data.compare_exchange_strong(exp, 2);
//结果 data=0, exp=0, flag=false

注意,compare_exchange_weak 判断时可能失败,实际相等却返回false(原因很多有可能是物理存储的问题,比较逻辑的问题或者函数执行失败(强制切出执行序列))这类失败叫做伪失败。与compare_exchange_weak 不同, strong版本的 compare-and-exchange 操作不允许伪失败返回 false,即只有比较操作的值不同才会返回false,这样能一定程度上消除对循环的依赖。不过在某些平台下,如果算法本身需要循环操作来做检查, compare_exchange_weak 的性能会更好。这里我研究的也不是很深入,以后遇到会补上。

5.2.4 std::atomic<T*> 上的操作:指针算数运算

对于指针的原子操作,同样不可拷贝构造和拷贝赋值的,可以从一个非原子的合适的指针来构造它,除了上述bool类型有的之外,还提供了指针运算,fetch_add()fetch_sub()+=-=++--。对于++和–的操作与普通指针相同,重点在fetch_add()fetch_sub()+=-= 之间的区别,下面一段代码将解释它们之间的区别。

int a[100];
std::atomic<int*> p = a;

//这里p会后移2位指向第三个元素,但是返回值是原来p的值,也就是x指向数组头
//fetch_add();可以接受具有任意的内存序列,默认接受memory_order_seq_cst
int* x = p.fetch_add(2);

///这里p会后移2位指向第三个元素,但是返回值是现在p的值,也就是x指向也是第三个元素
int* x = (p+=2);

//fetch_sub()和-=同理
5.2.5 标准原子整型的操作

除了一组通常的额操作(load()、store()、exchange()、compare_exchange_weak()和compare_exchange_strong())之外,像std::atomic<int>或者std::atomic<unsigned long long>这样的原子整型还有相当广泛的一组操作可用:fetch_add() fetch_sub() fetch_for() fetch_xor() 这些运算的复合形式(+=、-=、&=、|=和^=),前缀/后缀自增和前缀/后缀自减(++x、x++、–x、x–)。这并不是很完整的一组可以在普通的整型上进行的复合赋值运算,对于除法、乘法、和位移运算符是缺失的。因为原子整型值通常作为计数器或者位掩码来使用,这不是一个特别明显的损失。如果需要的话,可以通过在一个循环中使用compare_exchange_weak()来实现。

5.2.6 std::atomic<>初始类模板

除了标准的原子类型,初级模板的存在允许用户创建一个用户定义的类型的原子变种。然而,如果用户定义类型用于初级模板,那么该类型必须满足以下准则:

用户定义类型UDT使用std::atomic,这种类型必须有一个平凡(trivial)拷贝赋值运算符。这意味着该类型不得拥有任何虚函数或虚基类,并且必须使用编译器生成的拷贝赋值运算符。

一个用户定义类型的每个基类和非静态数据成员都必须有一个平凡的拷贝赋值运算符。这实质上允许编译器将memcpy()或一个等价的操作用于赋值操作,因为没有用户编写的代码要运行。

最后,该类型必须是按位相等可比较的。这伴随着赋值的要求,你不仅要能够使用memcpy()复制UDT类型的对象,而且还必须能够使用memcmp()比较实例是否相等。

一般情况下,编译器无法为std::atomic<UDT>生成无锁代码,所以它必须对所以的操作使用一个内部锁。如果用户提供的拷贝赋值或比较运算符是被允许的,这将需要传递一个受保护数据的引用作为一个用户提供的函数的参数,因而违背了准则(不能将受保护的数据的指针或引用传给外部)。最后这些限制增加了编译器能够直接为std::atomic<UDT>利用原子指令的机会(并因此使一个特定的实例无锁),因为它能够把用户定义类型作为一组原始字节来处理。

注意:虽然可以使用std::atomic<float>std::atomic<double>,因为内置的浮点类型确实满足与memcpy和memcmp一同使用的准则,但是在compare_exchange_strong情况下这种行为可能会令人惊讶。如果存储的值具有不同的表示,即使旧的存储的值与比较值数值相等也可能返回错误的结果。还有浮点原子类型没有算数操作。

另外,std::atomic<>初始类模板不允许创建包含:计数器、标识符、指针甚至简单的数据元素的数组的类一起使用。

5.2.7 原子操作的自由函数

原子操作的自由函数简单理解就是将成员函数功能用非成员函数实现出来,一般这种自由函数与成员函数的功能完全一致,在设计上为了兼容C,参数接收原子变量的指针而非引用。具体用法和细节这里不做介绍。

5.3 同步操作和强制顺序

5.3.1 synchronizes-with 关系

synchronizes-with (与之同步)关系是你只能在原子类型上的操作之间得到的东西。如果一个数据结构包含原子类型,并且该数据结构上的操作会在内部执行合法的原子操作,该数据结构上的操作(如锁定互斥元)可能会提供这种关系,但是从根本上说synchronizes-with关系只出自原子类型的操作上。

如果线程A存储一个值而线程B读取该值,那么线程A中的存储和线程B中的载入之间存在一种synchronizes-with关系。

5.3.2 happens-before 关系

happens-before(发生于之前)关系是程序中操作顺序的基本构件,它指定了哪些操作能看到其他操作的结果。对于单线程,如果一个操作A排在另一个操作B前面,那么A一定限于B执行,且在执行B的时候A是一定执行完毕的。但是如果操作发生在同一条语句中,一般是不具有happens-before关系的,例如:

#include <iostream>

void foo(int a, int b)
{
    std::cout << a << "," << b << std::endl;
}
int get_num()
{
    static int i = 0;
    return i++;
}
int main (int argc, char** argv)
{	//这里对get_num()调用是无序的,有可能是“1,2”也有可能是“2,1”
    foo(get_num(), get_num());
}

有时候单条语句中的操作是有序的,例如使用内置逗号操作符或者使用一个表达式的结果作为另一个表达式的参数。happens-before关系相当于强制规定顺序,比如某项操作一定要发生在某项操作后面,最简单的例子就是数据的读写,一般来说传递数据要求读一定要在写操作之后。

5.3.3 原子操作的内存顺序

有六种顺序选项可以应用到原子类型上的操作,它们分别代表三种模型:**顺序一致(sequentially consistent)**顺序、**获得-释放(acquire-release)顺序和松散(relaxed)**顺序:

memory_order_relaxed 松散

memory_order_consume 获得-释放

memory_order_acquire 获得-释放

memory_order_relesae 获得-释放

memory_order_acq_rel 获得-释放

memory_order_seq_cst 顺序一致

这些不同的内存顺序模型在不同CPU架构上可能有不同的成本。例如,在基于具有通过处理器而非做更改者对操作的可见性进行良好控制架构上的系统中(说人话就是处理器自主处理大多数操作并且对使用者不可见),顺序一致的顺序相对于获得-释放顺序或松散顺序,以及获得-释放顺序相对于松散顺序可能会要求额外的同步指令。如果这些系统拥有很多很多处理器,这些额外的同步指令可能占据显著的时间从而降低该系统的整体性能。另一方面,为了确保原子性,对于超出需要的获取-释放排序,使用x86或x86-64架构(如在台式PC中常见的Interl和AMD处理器)的CPU不会要求额外的指令,甚至对于载入操作,顺序一致顺序不需要任何特殊处理,尽管在存储的时候有一点额外的一点成本。

不同的内存顺序模型允许高手们利用更细粒度的顺序关系来提升性能,在不太关键的情况下,当允许使用默认一致顺序时是有优势的。

顺序一致顺序

默认的顺序被命名为顺序一致顺序,因为这意味着程序的行为与一个简单的顺序的世界观是一致的。如果所有原子类型实例上的操作是顺序一致的,多线程程序行为,就好像是所有这些操作由单个线程以某种特定的顺序进行执行一样。这是迄今为止最容易理解的内存顺序,这也是它作为默认值得原因。所有线程都必须看到操作得相同顺序,消除那些不一致得,并验证你的代码在其他程序里是否和预期的行为一样。这也意味着,操作不能被重排。如果你的代码在一个线程中有一个操作在另一个之前,其顺序必须对所有其他线程可见。

从同步的观点来看,顺序一致的存储与读取该存储值得同样一个变量得顺序一致载入是同步的。这提供了一种两个(或多个)线程操作的顺序约束,但顺序一致比它更加强大。在使用顺序一致原子操作的系统中,所有载入后完成的顺序一致原子操作,也必须出现在其他线程的存储之后。然而,易于理解就产生了代价。在一个带有许多处理器的弱顺序机器上,它可能导致显著的性能惩罚,因为操作的整体顺序必须与处理之间保持一致,可能需要处理器之间进行密集(且昂贵)的同步操作。这就是说,有些处理器架构(如常见的x86和x86-64架构)提供相对低廉的顺序一致性,因此如果你担心使用顺序一致顺序对性能的影响,那么就需要检擦一下使用的目标处理器架构的文档。

#include <iostream>
#include <atomic>
#include <thread>
#include <assert.h>

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

void write_x()
{
    x.store(true, std::memory_order_seq_cst);
}

void write_y()
{
    y.store(true, std::memory_order_seq_cst);
}

void read_x_then_y()
{
    while (!x.load(std::memory_order_seq_cst))
    {
        if (y.load(std::memory_order_seq_cst))
        {
            ++z;
        }   
    }
}

void read_y_then_x()
{
    while (!y.load(std::memory_order_seq_cst))
    {
        if (x.load(std::memory_order_seq_cst))
        {
            ++z;
        }       
    }
}

int main(int argc, const char** argv) {

    x = false;
    y = false;
    z = 0;
    std::thread a(write_x);
    std::thread b(write_y);
    std::thread c(read_x_then_y);
    std::thread d(read_y_then_x);

    a.join();
    b.join();
    c.join();
    d.join();

    assert(z.load() != 0);
    std::cout << "/* message */" << std::endl;
    return 0;
}

//这段代码执行可能出现两种情况 
/************************************
第一种:
a: 05_04.cpp:58: int main(int, const char**): Assertion `z.load() != 0' failed.
Aborted (core dumped)

第二种:
/* message */
************************************/

书上说z无论如何都不会等于0,但是就我在虚拟机上测试的结果来看,z为零的可能性还很大(基本10次有6/7次都是得0),看书上得意思是因为std::memory_order_seq_cst会强制顺序,它还会对所有拥有此标签的内存操作建立一个单独全序,和我们看到的顺序一致。载入(load)必须发生在存储(store)之前(???),反正结合我的实验结果我已经不明白书上写的是什么意思了,顺序一致到底是指什么顺序一致呢?如果载入(load)必须发生在存储(store)之前,那么不管是线程c还是线程d都会进到while循环种,且至少会有一个线程进入到if中,对z进行修改,所以一定会看到/* message */。但是显然我的测试结果不是这样,有很大可能在c线程和d线程没有load的时候x和y就被store了。

所以这段给我整不会了,望大佬指点。

松散顺序

以松散顺序执行的原子类型上的操作不参与synchronizes-with关系。对于单线程来说没有变化,但是对于多线程,其他线程的顺序几乎没有任何要求。唯一要求是,从同一个线程对单个原子变量的访问不能被重排,一旦给定的线程已经看到了原子变量的特定值,该线程之后的读取就不能获取该变量更早的值。在没有使用额外同步的情况下,每个变量的修改顺序是使用 memory_order_relaxed 的线程之间唯一共享的东西。

#include <iostream>
#include <atomic>
#include <thread>
#include <assert.h>

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

void write_x_then_y()
{
    x.store(true, std::memory_order_relaxed);

    y.store(true, std::memory_order_relaxed);
}

void read_y_then_x()
{
    while (!y.load(std::memory_order_relaxed))
    {
        if (x.load(std::memory_order_relaxed))
        {
            ++z;
        }     
    }
}

int main(int argc, const char** argv) {

    x = false;
    y = false;
    z = 0;
    std::thread a(write_x_then_y);
    std::thread b(read_y_then_x);

    a.join();
    b.join();
    assert(z.load() !=  0);
    
    return 0;
}

这次的assert确实可以触发(但是前面顺序一致的也能啊),所以我不确定书上描写的特性是否正确。简单理解就是:在原子类型上的操作以自由序列执行,没有任何同步关系,仅对此操作要求原子性。例如,在某一线程中,先写入A,再写入B。但是在多核处理器中观测到的顺序可能是先写入B,再写入A。自由内存顺序对于不同变量可以自由重排序。

获得-释放顺序
  • memory_order_acquire

使用memory_order_acquire的原子操作,当前线程的读写操作都不能重排到此操作之前。例如,某一线程先读取A,再读取B,再以memeory_order_acquire操作读取C,再读取D。在多核处理器中观测到的顺序D只能在C之前,不能出现先读取D,最后读取C的情况。但是,可能出现A或B重排到C之后的情况。
memory_order_acquire用于获取数据,放在读操作的最开始 。

  • memory_order_release

使用memory_order_release的原子操作,当前线程的读写操作都不能重排到此操作之后。例如,某一线程先写入A,再写入B,再以memeory_order_release操作写入C,再写入D。在多核处理器中观测到的顺序AB只能在C之前,不能出现C写入之后,A或B再写入的情况。但是,可能出现D重排到C之前的情况。

memory_order_release用于发布数据,放在写操作的最后。

“获取”与“释放”一般会成对出现,用来同步线程。

  • memory_order_acq_rel

memory_order_acq_rel带此内存顺序的读-改-写操作既是获得加载又是释放操作。没有操作能够从此操作之后被重排到此操作之前,也没有操作能够从此操作之前被重排到此操作之后。

  • memory_order_consume

memory_order_consume只会对其标识的对象保证该对象存储先行于那些需要加载该对象的操作。

小结

这一段顺序属实给我整懵了。。。不知道是书上写得有问题还是我编译运行环境有问题,反正就是对不上。希望有大佬帮忙解答疑惑,感谢!

先这样吧,这里暂时也不会用太深。
【下一篇】03 重修C++之并发实战6

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值