C++内存模型和原子类型操作

    摘自 《C++并发编程(中文版) 》 第5章 C++内存模型和原子类型操作

   推荐阅读(强烈推荐):https://zh.cppreference.com/w/cpp/atomic/memory_order

    这里从两方面来讲内存模型: 一方面是基本结构, 这个结构奠定了与内存相关的基础; 另一方面就是并发。 基本结构对于并发也是很重要的, 特别是当你阅读到底层原子操作的时候,所以我将会从基本结构讲起。 在C++中, 它与所有的对象和内存位置有关。

   这里只抽出一些基本的内容,做简单的介绍,更具体详细见《C++并发编程(中文版) 》

一 c++对象的基本结构的内存布局

      这个其实应该很熟悉,不在过多的叙述。跳转至:c++类的内存分布以及虚函数的实现,想要更深层的了解基本内存布局可以去看一本书叫做《深度探索C++对象模型》。

二 对象、 内存位置和并发

     这里我们主要讨论多线程并发下的c++内存模型。这部分对于C++的多线程应用来说是至关重要的: 所有东西都在内存中。 当两个线程访问不同(separate)的内存位置时, 不会存在任何问题, 一切都工作顺利。 而另一种情况下, 当两个线程访问同一(same)个内存位置, 你就要小心了。 如果没有线程更新内存位置上的数据, 那还好; 只读数据不需要保护或同步。 当有线程对内存位置上的数据进行修改, 那就有可能会产生条件竞争,为了避免条件竞争, 两个线程就需要一定的执行顺序。 第一种方式,使用互斥量来确定访问的顺序; 当同一互斥量在两个线程同时访问前被锁住, 那么在同一时间内就只有一个线程能够访问到对应的内存位置, 所以后一个访问必须在前一个访问之后。 另一种方式是使用原子操作(atmic operations)同步机制 决定两个线程的访问顺序。 当多于两个线程访问同一个内存地址时, 对每个访问这都需要定义一个顺序。如果不去规定两个不同线程对同一内存地址访问的顺序, 那么访问就不是原子的; 并且, 当两个线程都是“作者”时, 就会产生数据竞争和未定义行为。以下的声明由为重要: 未定义的行为是C++中最黑暗的角落。 根据语言的标准, 一旦应用中有任何未定义的行为, 就很难预料会发生什么事情; 因为, 未定义行为是难以预料的。 我就知道一个未定义行为的特定实例, 让某人的显示器起火的案例。 虽然, 这种事情应该不会发生在你身上, 但是数据竞争绝对是一个严重的错误, 并且需要不惜一切代价避免它。另一个重点是: 当程序中的对同一内存地址中的数据访问存在竞争, 你可以使用原子操作来避免未定义行为。 当然, 这不会影响竞争的产生——原子操作并没有指定访问顺序——但原子操作把程序拉回了定义行为的区域内。在我们了解原子操作前, 还有一个有关对象和内存地址的概念需要重点了解: 修改顺序。

1 同步发生

    “同步发生”关系是指: 只能在原子类型之间进行的操作。 例如对一个数据结构进行操作(对互斥量上锁), 如果数据结构包含有原子类型, 并且操作内部执行了一定的原子操作, 那么这些操作就是同步发生关系。 从根本上说, 这种关系只能来源于对原子类型的操作。“同步发生”的基本想法是: 在变量x进行适当标记的原子写操作W, 同步与对x进行适当标记的原子读操作, 读取的是W操作写入的内容; 或是在W之后, 同一线程上的原子写操作对x写入的值; 亦或是任意线程对x的一系列原子读-改-写操作(例如, fetch_add()或compare_exchange_weak())。 这里, 第一个线程读取到的值是W操作写入的。先将“适当的标记”放在一边, 因为所有对原子类型的操作, 默认都是适当标记的。 这实际上就是: 如果线程A存储了一个值, 并且线程B读取了这个值, 线程A的存储操作与线程B的载入操作就是同步发生的关系。

2 先行发生

      “先行发生”关系是一个程序中, 基本构建块的操作顺序; 它指定了某个操作去影响另一个操作。 对于单线程来说, 就简单了: 当一个操作排在另一个之后, 那么这个操作就是先行执行的。 这意味着, 如果源码中操作A发生在操作B之前, 那么A就先行与B发生。

3 原子操作的内存序列
       这里有六个内存序列选项可应用于对原子类型的操作: memory_order_relaxed,memory_order_consume, memory_order_acquire, memory_order_release,memory_order_acq_rel, 以及memory_order_seq_cst。 除非你为特定的操作指定一个序列选项, 要不内存序列选项对于所有原子类型默认都是memory_order_seq_cst。

      虽然有六个选项, 但是它们仅代表三种内存模型: 排序一致序列(sequentially consistent), 获取-释放序列(memory_order_consume, memory_order_acquire, memory_order_release和memory_order_acq_rel 三者的区别请仔细看文章开头的推荐文章链接==>推荐阅读), 和自由序列(memory_order_relaxed)。这些不同的内存序列模型, 在不同的CPU架构下, 功耗是不一样的。 例如, 基于处理器架构的可视化精细操作的系统, 比起其他系统, 添加的同步指令可被排序一致序列使用(在获取-释放序列和自由序列之前), 或被获取-释放序列调用(在自由序列之前)。 如果这些系统有多个处理器, 这些额外添加的同步指令可能会消耗大量的时间, 从而降低系统整体的性能。 另一方面, CPU使用的是x86或x86-64架构(例如, 使用Intel或AMD处理器的台式电脑), 使用这种架构的CPU不需要任何对获取-释放序列添加额外的指令(没有保证原子性的必要了), 并且, 即使是排序一致序列, 对于加载操作也不需要任何特殊的处理, 不过在进行存储时, 有点额外的消耗。不同种类的内存序列模型, 允许专家利用其提升与更细粒度排序相关操作的性能。 当默认使用排序一致序列(相较于其他序列, 它是最简单的)时, 对于在那些不大重要的情况下是有利的。选择使用哪个模型, 或为了了解与序列相关的代码, 为什么选择不同的内存模型, 是需要了解一个重要的前提, 那就是不同模型是如何影响程序的行为。 让我们来看一下选择每个操作序列和同步相关的结果。

  1) 排序一致队列
    默认序列命名为“排序一致”(sequentially cons), 是因为它意味着, 程序中的行为从任意角度去看, 序列顺序都保持一致。 如果原子类型实例上的所有操作都是序列一致的, 那么一个多线程程序的行为, 就以某种特殊的排序执行, 好像单线程那样。 这是目前来看, 最容易理解的内存序列, 这也就是将其设置为默认的原因: 所有线程都必须了解, 不同的操作也遵守相同的顺序。 因为其简单的行为, 可以使用原子变量进行编写。 通过不同的线程, 你可以写出所有序列上可能的操作, 这样就可以消除那些不一致, 以及验证你代码的行为是否与预期相符。 这也就意味着, 所有操作都不能重排序; 如果你的代码, 在一个线程中, 将一个操作放在另一个操作前面, 那么这个顺序就必须让其他所有的线程所了解。从同步的角度看, 对于同一变量, 排序一致的存储操作同步相关于同步一致的载入操作。 这就提供了一种对两个(以上)线程操作的排序约束, 但是排序一致的功能要比排序约束大的多。所以, 对于使用排序一致原子操作的系统上的任一排序一致的原子操作, 都会在对值进行存储以后, 再进行加载。 清单5.4就是这种一致性约束的演示。 这种约束不是线程在自由内存序列中使用原子操作; 这些线程依旧可以知道, 操作以不同顺序排列, 所以你必须使用排序一致操作, 去保证在多线的情况下有加速的效果。不过, 简单是要付出代价的。 在一个多核若排序的机器上, 它会加强对性能的惩罚, 因为整个序列中的操作都必须在多个处理器上保持一致, 可能需要对处理器间的同步操作进行扩展(代价很昂贵! )。 即便如此, 一些处理器架构(比如通用x86和x86-64架构)就提供了相对廉价的序列一致, 所以你需要考虑使用序列一致对性能的影响, 这就需要你去查阅你目标处理器的架构文档, 进行更多的了解。以下清单展示了序列一致的行为, 对于x和y的加载和存储都显示标注为memory_order_seq_cst, 不过在这段代码中, 标签可能会忽略, 因为其是默认项。

//代码 5.4
#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); // 1
}

void write_y()
{
	y.store(true,std::memory_order_seq_cst); // 2
} 
void read_x_then_y()
{
	while(!x.load(std::memory_order_seq_cst));
	if(y.load(std::memory_order_seq_cst)) // 3
	++z;
} 
void read_y_then_x()
{
	while(!y.load(std::memory_order_seq_cst));
	if(x.load(std::memory_order_seq_cst)) // 4
	++z;
} 
int main()
{
	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); // 5
}

    assert⑤语句是永远不会触发的, 因为不是存储x的操作①发生, 就是存储y的操作②发生。 如果在read_x_then_y中加载y③返回false, 那是因为存储x的操作肯定发生在存储y的操作之前, 那么在这种情况下在read_y_then_x中加载x④必定会返回true, 因为while循环能保证在某一时刻y是true。 因为memory_order_seq_cst的语义需要一个单全序将所有操作都标记为
memory_order_seq_cst, 这就暗示着“加载y并返回false③”与“存储y①”的操作, 有一个确定的顺序。 只有一个全序时, 如果一个线程看到x==true, 随后又看到y==false, 这就意味着在总序列中存储x的操作发生在存储y的操作之前。当然, 因为所有事情都是对称的, 所以就有可能以其他方式发生, 比如, 加载x④的操作返回false, 或强制加载y③的操作返回true。 在这两种情况下, z都等于1。 当两个加载操作都返回true, z就等于2, 所以任何情况下, z都不能是0。当read_x_then_y知道x为true, 并且y为false, 那么这些操作就有“先发执行”关系了, 如图所示。
      虚线始于read_x_then_y中对y的加载操作, 到达write_y中对y的存储, 其暗示了排序关系需要保持序列一致: 在操作的全局操作顺序memory_order_seq_cst中, 加载操作必须在存储操作之前发生, 就产生了图中的结果。序列一致是最简单、 直观的序列, 但是他也是最昂贵的内存序列, 因为它需要对所有线程进行全局同步。 在一个多处理系统上, 这就需要处理期间进行大量并且费时的信息交换。为了避免这种同步消耗, 你需要走出序列一致的世界, 并且考虑使用其他内存序列.

 2) 自由序列

     当你踏出序列一致的世界, 所有事情就开始变的复杂。 可能最需要处理的问题就是: 再也不会有全局的序列了(there’s no longer a single global order of events)。 这就意味着不同线程看到相同操作, 不一定有着相同的顺序, 还有对于不同线程的操作, 都会整齐的, 一个接着另一个执行的想法是需要摒弃的。 不仅是你有没有考虑事情真的同时发生的问题, 还有线程没必要去保证一致性(threads don’t have to agree on the order of events)。 为了写出(或仅是了解)任何一段使用非默认内存序列的代码, 要想做这件事情, 那么之前的那句话就是至关重要的。 你要知道, 这不仅仅是编译器可以重新排列指令的问题。 即使线程运行相同的代码, 它们都能拒绝遵循事件发生的顺序, 因为操作在其他线程上没有明确的顺序限制; 因为不同的CPU缓存和内部缓冲区, 在同样的存储空间中可以存储不同的值。 这非常重要, 这里我再重申一遍: 线程没必要去保证一致性。

     不仅是要摒弃交错执行操作的想法, 你还要放弃使用编译器或处理器重排指令的想法。 在没有明确的顺序限制下, 唯一的要求就是, 所有线程都要统一对每一个独立变量的修改顺序。对不同变量的操作可以体现在不同线程的不同序列上, 提供的值要与任意附加顺序限制保持一致。踏出排序一致世界后, 最好的示范就是使用memory_order_relaxed对所有操作进行约束。 如果你已经对其有所了解, 那么你可以跳到获取-释放序列继续阅读, 获取-释放序列允许你选择在操作间引入顺序关系(并且收回你的理智)。

    在原子类型上的操作以自由序列执行, 没有任何同步关系。 在同一线程中对于同一变量的操作还是服从先发执行的关系, 但是这里不同线程几乎不需要相对的顺序。 唯一的要求是, 在访问同一线程中的单个原子变量不能重排序; 当一个给定线程已经看到一个原子变量的特定值, 线程随后的读操作就不会去检索变量较早的那个值。 当使用memory_order_relaxed, 就不需要任何额外的同步, 对于每个变量的修改顺序只是线程间共享的事情。为了演示如何不去限制你的非限制操作, 你只需要两个线程, 就如同下面代码清单5.5那样。

//代码 5.5
#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); // 1
	y.store(true,std::memory_order_relaxed); // 2
} 
void read_y_then_x()
{
	while(!y.load(std::memory_order_relaxed)); // 3
	if(x.load(std::memory_order_relaxed)) // 4
	++z;
} 
int main()
{
	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); // 5
}

    这次assert⑤可能会触发, 因为加载x的操作④可能读取到false, 即使加载y的操作③读取到true, 并且存储x的操作①先发与存储y的操作②。 x和y是两个不同的变量, 所以这里没有顺序去保证每个操作产生相关值的可见性。非限制操作对于不同变量可以自由重排序, 只要它们服从任意的先发执行关系即可(比如, 在同一线程中)。 它们不会引入同步相关的顺序。 清单5.5中的先发执行关系如图5.4所示(只是其中一个可能的结果)。 尽管, 在不同的存储/加载操作间有着先发执行关系, 这里不是在一对存
储于载入之间了, 所以载入操作可以看到“违反”顺序的存储操作

 3) 获取释放序列

      这个序列是自由序列(relaxed ordering)的加强版; 虽然操作依旧没有统一的顺序, 但是在这个序列引入了同步。 在这种序列模型中, 原子加载就是“获取”(acquire)操作(memory_order_acquire), 原子存储就是“释放”操作(memory_order_release), 原子读-改-写操作(例如fetch_add()或exchange())在这里, 不是“获取”, 就是“释放”, 或者两者兼有的操(memory_order_acq_rel)。 这里, 同步在线程释放和获取间, 是成对的(pairwise)。 释放操作与获取操作同步, 这样就能读取已写入的值。 这意味着不同线程看到的序列虽还是不同, 但这些序列都是受限的。 下面列表中是使用获取-释放序列(而非序列一致方式), 下面对上面5.4的一次重写。

//代码 5.7
#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_release);
} 
void write_y()
{
	y.store(true,std::memory_order_release);
}
void read_x_then_y()
{
	while(!x.load(std::memory_order_acquire));
	if(y.load(std::memory_order_acquire)) // 1
	++z;
} 
void read_y_then_x()
{
	while(!y.load(std::memory_order_acquire)); // 2
	if(x.load(std::memory_order_acquire))
	++z;
}
int main()
{
	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); // 3
}

在这个例子中断言3可能会触发(就如同自由排序那样), 因为可能在加载x  //1和y //2的时候, 读取到的是false。 因为x和y是由不同线程写入, 所以序列中的每一次释放到获取都不会影响到其他线程的操作。下图展示了先行关系, 对于读取的结果, 两个(读取)线程看到的是两个完全不同的世界。 如前所述, 这可能是因为这里没有对先行顺序进行强制规定导致的。



   为了了解获取-释放序列有什么优点, 你需要考虑将两次存储由一个线程来完成, 就像清单5.5那样。 当你需要使用memory_order_release改变y中的存储, 并且使用memory_order_acquire来加载y中的值, 就像下面程序清单所做的那样, 而后, 就会影响到序列中对x的操作。

//代码 5.8
#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); // 1 自旋, 等待y被设置为true
	y.store(true,std::memory_order_release); // 2
} 
void read_y_then_x()
{
	while(!y.load(std::memory_order_acquire)); // 3
	if(x.load(std::memory_order_relaxed)) // 4
	++z;
}
int main()
{
	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); // 5
}

     最后, 读取y③时会得到true, 和存储时写入的一样②。 因为存储使用的是memory_order_release, 读取使用的是memory_order_acquire, 存储就与读取就同步了。 因为这两个操作是由同一个线程完成的, 所以存储x①先行与加载y②。 对y的存储同步与对y的加载, 存储x也就先行于对y的加载, 并且扩展先行与x的读取。 因此, 加载x的值必为true, 并且断言⑤不会触发。 如果对于y的加载不是在while循环中, 那么情况可能就会有所不同; 加载y的时候可能会读取到false, 在这种情况下对于读取到的x是什么值, 就没有要求了。 为了保证同步, 加载和释放操作必须成对。 所以, 无论有何影响, 释放操作存储的值, 必须要让获取操作看到。 当存储如②或加载如③, 都是一个释放操作时, 对x的访问就无序了, 也就无法保
证④处读到的是true, 并且还会触发断言。

     总结来说获取释放序列对应两种操作:

     1 Release -- acquire: 线程 A 原子性地把值写入 x (release), 然后线程 B 原子性地读取 x 的值(acquire). 这样线程 B 保证读取到 x 的最新值。注意 release -- acquire 有一个特别且重要的作用:线程 A 中所有发生在 release x 之前的写操作,对在线程 B acquire x 之后的任何读操作都可见!本来 A, B 间读写操作顺序不定。这么一同步,在 x 这个点前后, A, B 线程之间有了个顺序关系,称作 inter-thread happens-before。
     2. Release -- consume: 相对于第一种,如果我只想同步 x 的读写操作,结果把 release x 之前的写操作都顺带同步了?如果我想避免这个额外开销怎么办?那就可以用 release -- consume 。它还是跟上面一样的同步,但是保护性弱了点:在线程 B acquire x 之后的读操作中,有一些是依赖于 x 的值的读操作。管这些依赖于 x 的读操作叫 赖B读. 同理在线程 A 里面, release x 也有一些它所依赖的其他写操作,这些写操作自然发生在 release x 之前了。管这些写操作叫 赖A写. 现在这个副作用就是,只有 赖B读的操作 能看见 赖A写操作,除了线程b的赖B读的操作和线程a的赖A写操作其他的不保证一致性,可能被reorder。

三 栅栏(内存屏障)

      往下浏览之前先了解下内存屏障,点击查看了解内存屏障

      如果原 子操作库缺少了栅栏, 那么这个库就是不完整的。 栅栏操作会对内存序列进行约束,使其无法对任何数据进行修改, 典型的做法是与使用memory_order_relaxed约束序的原子操作一起使用。 栅栏属于全局操作, 执行栅栏操作可以影响到在线程中的其他原子操作。 因为这类操作就像画了一条任何代码都无法跨越的线一样, 所以栅栏操作通常也被称为“内存栅栏”(memory barriers)。 回忆一下5.3.3节, 自由操作可以使用编译器或者硬件的方式, 在独立的变量上自由的进行重新排序。 不过, 栅栏操作就会限制这种自由, 并且会介绍之前没有介绍到的“先行”和“同步”关系。我们给在不同线程上的两个原子操作中添加一个栅栏, 代码如下所示:

     

//代码 5.12
#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); // 1
	std::atomic_thread_fence(std::memory_order_release); // 2
	y.store(true,std::memory_order_relaxed); // 3
}
 void read_y_then_x()
{
	while(!y.load(std::memory_order_relaxed)); // 4
	std::atomic_thread_fence(std::memory_order_acquire); // 5
	if(x.load(std::memory_order_relaxed)) // 6
	++z;
} 
int main()
{
	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); // 7
}

     释放栅栏②与获取栅栏⑤同步, 这是因为加载y的操作④读取的是在③处存储的值。 所以, 在①处存储x先行与⑥处加载x, 最后x读取出来必为true, 并且断言不会被触发⑦。 原先不带栅栏的存储和加载x都是无序的, 并且断言是可能会触发的。 需要注意的是, 这两个栅栏都是必要的: 你需要在一个线程中进行释放, 然后在另一个线程中进行获取, 这样才能构建出同步关
系。
     在这个例子中, 如果存储y的操作③标记为memory_order_release, 而非memory_order_relaxed的话, 释放栅栏②也会对这个操作产生影响。 同样的, 当加载y的操作④标记为memory_order_acquire时, 获取栅栏⑤也会对之产生影响。 使用栅栏的一般想法是: 当一个获取操作能看到释放栅栏操作后的存储结果, 那么这个栅栏就与获取操作同步;并且, 当加载操作在获取栅栏操作前, 看到一个释放操作的结果, 那么这个释放操作同步于获取栅栏。 当然, 你也可以使用双边栅栏操作, 举一个简单的例子, 当一个加载操作在获取栅栏前, 看到一个值有存储操作写入, 且这个存储操作发生在释放栅栏后, 那么释放栅栏与
获取栅栏是同步的。

   虽然, 栅栏同步依赖于读取/写入的操作发生于栅栏之前/后, 但是这里有一点很重要: 同步点, 就是栅栏本身。 当你执行清单5.12中的write_x_then_y, 并且在栅栏操作之后对x进行写入, 就像下面的代码一样。 这里, 触发断言的条件就不保证一定为true了, 尽管写入x的操作在写入y的操作之前发生。

void write_x_then_y()
{
    std::atomic_thread_fence(std::memory_order_release);
    x.store(true,std::memory_order_relaxed);
    y.store(true,std::memory_order_relaxed);
}

    这里里的两个操作, 就不会被栅栏分开, 并且也不再有序。 只有当栅栏出现在存储x和存储y操作之间, 这个顺序是硬性的。 当然, 栅栏是否存在不会影响任何拥有先行关系的执行序列, 这种情况是因为一些其他原子操作。这个例子, 以及本章中的其他例子, 变量使用的都是完整的原子类型。 不过, 正真的好处在于, 使用原子操作去执行一个序列, 可以避免对于一些数据竞争的未定义行为。

    再看一个栅栏对非原子的操作排序

#include <atomic>
#include <thread>
#include <assert.h>
bool x=false; // x现在是一个非原子变量
std::atomic<bool> y;
std::atomic<int> z;
void write_x_then_y()
{
	x=true; // 1 在栅栏前存储x
	std::atomic_thread_fence(std::memory_order_release);
	y.store(true,std::memory_order_relaxed); // 2 在栅栏后存储y
} 
void read_y_then_x()
{
	while(!y.load(std::memory_order_relaxed)); // 3 在#2写入前, 持续等待
	std::atomic_thread_fence(std::memory_order_acquire);
	if(x) // 4 这里读取到的值, 是#1中写入
	++z;
} 
int main()
{
	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); // 5 断言将不会触发
}

     栅栏仍然为存储x①和存储y②, 还有加载y③和加载x④提供一个执行序列, 并且这里仍然有一个先行关系, 在存储x和加载x之间, 所以断言⑤不会被触发。 ②中的存储和③中对y的加载,都必须是原子操作; 否则, 将会在y上产生条件竞争, 不过一旦读取线程看到存储到y的操作, 栅栏将会对x执行有序的操作。 这个执行顺序意味着, x上不存在条件竞争, 即使它被另外的线程修改或被其他线程读取。

C++11中的内存模型上篇 - 内存模型基础

C++11中的内存模型下篇 - C++11支持的几种内存模型

C++ memory order循序渐进(四)—— 在std::atomic_thread_fence 上应用std::memory_order实现不同的内存序

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值