Update item40.md

This commit is contained in:
猫耳堀川雷鼓 2021-03-12 16:52:56 +08:00 committed by GitHub
parent 9d39df9f35
commit b5f0476254
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

View File

@ -1,74 +1,74 @@
## Item 40Use std::atomic for councurrency, volatile for special memory ## 条款四十:对于并发使用`std::atomic`,对于特殊内存使用`volatile`
Item 40: 当需要并发时使用`std::atomic`,特定内存才使用`volatile` **Item 40: Use `std::atomic` for concurrency, `volatile` for special memory**
伶的`volatile`。如此令人迷惑。本不应该出现在本章节,因为它没有关于并发的能力。但是在其他编程语言中比如Java和C#`volatile`是有并发含义的即使在C++中,有些编译器在实现时也将并发的某种含义加入到了`volatile`关键字中。因此在此值得讨论下关于`volatile`关键字的含义以消除异议。 怜的`volatile`。如此令人迷惑。本不应该出现在本章节,因为它跟并发编程没有关系。但是在其他编程语言中比如Java和C#`volatile`是有并发含义的即使在C++中,有些编译器在实现时也将并发的某种含义加入到了`volatile`关键字中(但仅仅是在用那些编译器时)。因此在此值得讨论下关于`volatile`关键字的含义以消除异议。
开发者有时会混淆`volatile`的特性是`std::atomic`(这确实本节的内容)的模板。这种模板的实例化(比如,`std::atomic<int> , std::atomic<bool>, std::atomic<Widget*>`等)给其他线程提供了原子操作的保证。一旦`std::atomic`对象被构建,在其上的操作使用特定的机器指令实现,这比锁的实现更高效。 开发者有时会与`volatile`混淆的特性——本来应该属于本章的那个特性——是`std::atomic`模板。这种模板的实例化(比如,`std::atomic<int>``std::atomic<bool>``std::atomic<Widget*>`等)提供了一种在其他线程看来操作是原子性的的保证(译注:即某些操作是像原子一样的不可分割。)。一旦`std::atomic`对象被构建,在其上的操作表现得像操作是在互斥锁保护的关键区内,但是通常这些操作是使用特定的机器指令实现,这比锁的实现更高效。
分析如下使用`std::atmoic`的代码: 分析如下使用`std::atmoic`的代码:
```cpp ```cpp
std::atomic<int> ai(0); // initialize ai to 0 std::atomic<int> ai(0); //初始化ai为0
ai = 10; // atomically set ai to 10 ai = 10; //原子性地设置ai为10
std::cout << ai; // atomically read ai's value std::cout << ai; //原子性地读取ai的值
++ai; //atomically increment ai to 11 ++ai; //原子性地递增ai到11
--ai; // atomically decrement ai to 10 --ai; //原子性地递减ai到10
``` ```
在这些语句执行过程中,其他线程读取`ai`只能读取到01011三个值其中一个。在没有其他线程修改`ai`情况下,没有其他可能 在这些语句执行过程中,其他线程读取`ai`只能读取到01011三个值其中一个。没有其他可能(当然,假设只有这个线程会修改`ai`
这个例子中有两点值得注意。**首先**,在`std::cout << ai;``std::atomic`只保证了对`ai`的读取时原子的没有保证语句的整个执行是原子的这意味着在读取`ai`与将其通过``操作符写入到标准输出之间另一个线程可能会修改`ai`的值这对于这个语句没有影响因为`<<`操作符是按值传递参数的所以输出就是读取到的`ai`的值但是重要的是要理解原子性的范围只保证了读取是原子的 这个例子中有两点值得注意。首先,在`std::cout << ai;``ai`是一个`std::atomic`的事实只保证了对`ai`的读取是原子的没有保证整个语句的执行是原子的在读取`ai`的时刻与调用`operator<<`将值写入到标准输出之间另一个线程可能会修改`ai`的值这对于这个语句没有影响因为`int``operator<<`是使用`int`型的传值形参来输出所以输出的值就是读取到的`ai`的值但是重要的是要理解原子性的范围只保证了读取`ai`是原子
第二点值得注意的是最后两条语句---关于`ai`的加减。他们都是 read-modify-writeRMW操作各自原子执行。这是`std::atomic`类型的最优的特性之一:一旦`std::atomic`对象被构建所有成员函数包括RMW操作对于其他线程来说保证原子执行 第二点值得注意的是最后两条语句——关于`ai`的递增递减。他们都是读-改-写read-modify-writeRMW操作它们整体作为原子执行。这是`std::atomic`类型的最优的特性之一:一旦`std::atomic`对象被构建所有成员函数包括RMW操作从其他线程来看都是原子性的
相反,使用`volatile`在多线程中不保证任何事情: 相反,使用`volatile`在多线程中实际上不保证任何事情:
```cpp ```cpp
volatile int vi(0); // initalize vi to 0 volatile int vi(0); //初始化vi为0
vi = 10; // set vi to 10 vi = 10; //设置vi为10
std::cout << vi; // read vi's value std::cout << vi; //读vi的值
++vi; // increment vi to 11 ++vi; //递增vi到11
--vi; // decrement vi to 10 --vi; //递减vi到10
``` ```
代码的执行过程中,如果其他线程读取`vi`,可能读到任何值,比如-12684090727。这份代码就是未定义的,因为这里的语句修改`vi`,同时其他线程读取,这就是有没有`std::atomic`或者互斥锁保护的对于内存的同时读写,这就是数据竞争的定义。 代码的执行过程中,如果其他线程读取`vi`,可能读到任何值,比如-12684090727——任何值!这份代码有未定义行为,因为这里的语句修改`vi`,所以如果同时其他线程读取`vi`同时存在多个readers和writers读取没有`std::atomic`或者互斥锁保护的内存,这就是数据竞争的定义。
为了举一个关于在多线程程序中`std::atomic`和`volatile`表现不同的恰当例子,考虑这样一个加单的计数器,同时初始化为0 举一个关于在多线程程序中`std::atomic`和`volatile`表现不同的具体例子,考虑这样一个简单的计数器,通过多线程递增。我们把它们初始化为0
```cpp ```cpp
std::atomic<int> ac(0); std::atomic<int> ac(0); //“原子性的计数器”
volatile int vc(0); volatile int vc(0); //“volatile计数器”
``` ```
然后我们在两个同时运行的线程中对两个计数器计数 然后我们在两个同时运行的线程中对两个计数器递增
```cpp ```cpp
/*--------- Thread1 ---------*/ /*---------- Thread2 -----------*/ /*----- Thread 1 ----- */ /*------- Thread 2 ------- */
++ac; ++ac; ++ac; ++ac;
++vc; ++vc; ++vc; ++vc;
``` ```
当两个线程执行结束时,`ac`的值肯定是2以为每个自增操作都是原子的。另一方面,`vc`的值不一定是2因为自增不是原子的。每个自增操作包括了读取`vc`的值,增加读取的值,然后将结果写回到`vc`。这三个操作对于`volatile`修饰的整形变量不能保证原子执行,所有可能是下面的执行顺序: 当两个线程执行结束时,`ac`的值(即`std::atomic`的值肯定是2因为每个自增操作都是不可分割的原子性的。另一方面,`vc`的值不一定是2因为自增不是原子的。每个自增操作包括了读取`vc`的值,增加读取的值,然后将结果写回到`vc`。这三个操作对于`volatile`对象不能保证原子执行,所有可能是下面的交叉执行顺序:
1. Thread1 读取`vc`的值是0 1. Thread1读取`vc`的值是0
2. Thread2读取`vc`的值还是0 2. Thread2读取`vc`的值还是0
3. Thread1 将0加1然后写回到`vc` 3. Thread1将读到的0加1然后写回到`vc`
4. Thread2将0加1然后写回到vc 4. Thread2将读到的0加1然后写回到`vc`。
`vc`的最后结果是1即使看起来自增了两次。 `vc`的最后结果是1即使看起来自增了两次。
不仅只有这一种执行顺序的可能,`vc`的最终结果是不可预测的,因为`vc`会发生数据竞争,标准规定数据竞争的造成的未定义行为表示编译器生成的代码可能是任何逻辑,当然,编译器不会利用这种行为来作恶。但是只有在没有数据竞争的程序中编译器的优化才有效,这些优化在存在数据竞争的程序中会造成异常和不可预测的行为。 不仅只有这一种可能的结果通常来说`vc`的最终结果是不可预测的,因为`vc`会发生数据竞争,对于数据竞争造成未定义行为,标准规定表示编译器生成的代码可能是任何逻辑。当然,编译器不会利用这种行为来作恶。但是它们通常做出一些没有数据竞争的程序中才有效的优化,这些优化在存在数据竞争的程序中会造成异常和不可预测的行为。
RMW操作不是仅有的`std::atomic`在并发中有效而`volatile`无效的例子。假定一个任务计算第二个任务需要的重要值。当第一个任务完成计算必须传递给第二个任务。Item 39表明一种使用`std::atomic<bool>`的方法来使第一个任务通知第二个任务计算完成。代码如下: RMW操作不是仅有的`std::atomic`在并发中有效而`volatile`无效的例子。假定一个任务计算第二个任务需要的一个重要值。当第一个任务完成计算,必须传递给第二个任务。[Item39]()表明一种使用`std::atomic<bool>`的方法来使第一个任务通知第二个任务计算完成。计算值的任务的代码如下:
```cpp ```cpp
std::atomic<bool> valVailable(false); std::atomic<bool> valVailable(false);
auto imptValue = coputeImportantValue(); // compute value auto imptValue = computeImportantValue(); //计算值
valAvailable = true; // tell other task it's vailable valAvailable = true; //告诉另一个任务,值可用了
``` ```
人类读这份代码,能看到在`valAvailable`赋值true之前对`imptValue`赋值是重要的顺序,但是所有编译器看到的是一对没有依赖关系的赋值操作。通常来说,编译器会被允许重排这对没有关联的操作。这意味着,给定如下顺序的赋值操作 人类读这份代码,能看到在`valAvailable`赋值之前对`imptValue`赋值很关键,但是所有编译器看到的是给相互独立的变量的一对赋值操作。通常来说,编译器会被允许重排这对没有关联的操作。这意味着,给定如下顺序的赋值操作(其中`a``b``x``y`都是互相独立的变量),
```cpp ```cpp
a = b; a = b;
@ -82,30 +82,30 @@ x = y;
a = b; a = b;
``` ```
即使编译器没有重排顺序,底层硬件也可能重排,因为有时这样代码执行更快。 即使编译器没有重排顺序,底层硬件也可能重排(或者可能使它看起来运行在其他核心上),因为有时这样代码执行更快。
然而,`std::atomic`会限制这种重排序,并且这样的限制之一是,在源代码中,对`std::atomic`变量写之前不会有任何操作。这意味对我们的代码 然而,`std::atomic`会限制这种重排序,并且这样的限制之一是,在源代码中,对`std::atomic`变量写之前不会有任何操作(或者操作发生在其他核心上)(这只在`std::atomic`s使用**顺序一致性***sequential consistency*)时成立,对于使用在本书中展示的语法的`std::atomic`对象这也是默认的和唯一的一致性模型。C++11也支持带有更灵活的代码重排规则的一致性模型。这样的**弱***weak*)(亦称**松散的***relaxed*)模型使构建一些软件在某些硬件构架上运行的更快成为可能,但是使用这样的模型产生的软件**更加**难改正、理解、维护。在使用松散原子性的代码中微小的错误很常见,即使专家也会出错,所以应当尽可能坚持顺序一致性。)这意味对我们的代码
```cpp ```cpp
auto impatValue = computeImportantValue(); auto imptValue = computeImportantValue(); //计算值
valVailable = true; valAvailable = true; //告诉另一个任务,值可用了
``` ```
编译器不仅要保证赋值顺序,还要保证生成的硬件代码不会改变这个顺序。结果就是,将`valAvaliable`声明为`std::atomic`确保了必要的顺序---- 其他线程看到`imptValue`值保证`valVailable`设为true之后 编译器不仅要保证`imptValue`和`valAvailable`的赋值顺序,还要保证生成的硬件代码不会改变这个顺序。结果就是,将`valAvailable`声明为`std::atomic`确保了必要的顺序——其他线程看到的是`imptValue`值的改变不会晚于`valAvailable`
声明为`volatile`不能保证上述顺序: 将`valAvailable`声明为`volatile`不能保证上述顺序:
```cpp ```cpp
volatile bool valAvaliable(false); volatile bool valVailable(false);
auto imptValue = computeImportantValue(); auto imptValue = computeImportantValue();
valAvailable = true; valAvailable = true; //其他线程可能看到这个赋值操作早于imptValue的赋值操作
``` ```
这份代码编译器可能将赋值顺序对调,也可能在生成机器代码时,其他核心看到`valVailable`更改在`imptValue`之前。 这份代码编译器可能将`imptValue`和`valAvailable`赋值顺序对调,如果它们没这么做,可能不能生成机器代码,来阻止底部硬件在其他核心上的代码看到`valAvailable`更改在`imptValue`之前。
-------- 这两个问题——不保证操作的原子性以及对代码重排顺序没有足够限制——解释了为什么`volatile`在多线程编程中没用,但是没有解释它应该用在哪。简而言之,它是用来告诉编译器,它们处理的内存有不正常的表现。
“正常”内存应该有这个特性,在写入值之后,这个值会一直保证直到被覆盖。假设有这样一个正常的int “正常”内存应该有这个特性,在写入值之后,这个值会一直保持直到被覆盖。假设有这样一个正常的`int`
```cpp ```cpp
int x; int x;
@ -114,141 +114,141 @@ int x;
编译器看到下列的操作序列: 编译器看到下列的操作序列:
```cpp ```cpp
auto y = x; // read x auto y = x; //读x
y = x; // read x again y = x; //再次读x
``` ```
编译器可通过忽略对y的一次赋值来优化代码因为初始化和赋值是冗余的。 编译器可通过忽略对`y`的一次赋值来优化代码,因为有了`y`初始化,赋值是冗余的。
正常内存还有一个特征,就是如果你写入内存没有读取,再次写入,第一次写就可以被忽略,因为肯定会被覆盖。给出下面的代码: 正常内存还有一个特征,就是如果你写入内存没有读取,再次写入,第一次写就可以被忽略,因为值没被用过。给出下面的代码:
```cpp ```cpp
x = 10; // write x x = 10; //写x
x = 20; // write x again x = 20; //再次写x
``` ```
编译器可以忽略第一次写入。这意味着如果写在一起: 编译器可以忽略第一次写入。这意味着如果写在一起:
```cpp ```cpp
auto y = x; auto y = x; //读x
y = x; y = x; //再次读x
x = 10; x = 10; //写x
x = 20; x = 20; //再次写x
``` ```
编译器生成的代码是这样的: 编译器生成的代码是这样的:
```cpp ```cpp
auto y = x; auto y = x; //读x
x = 20; x = 20; //写x
``` ```
可能你会想谁会写这种重复读写的代码(技术上称为redundant loads 和 dead stores答案是开发者不会直接写至少我们不希望开发者这样写。但是在编译器执行了模板实例化,内联和一系列重排序优化之后,结果会出现多余的操作和无效存储,所以编译器需要摆脱这样的情况并不少见。 可能你会想谁会写这种重复读写的代码(技术上称为**冗余访问***redundant loads*)和**无用存储***dead stores*)),答案是开发者不会直接写——至少我们不希望开发者这样写。但是在编译器拿到看起来合理的代码,执行了模板实例化,内联和一系列重排序优化之后,结果会出现冗余访问和无用存储,所以编译器需要摆脱这样的情况并不少见。
这种有话讲仅仅在内存表现正常时有效。“特殊”的内存不行。最常见的“特殊”内存是用来memory-mapped I/O的内存。这种内存实际上是与外围设备比如外部传感器或者显示器打印机网络端口通信而不是读写比如RAM。这种情况下再次考虑多余的代码: 这种优化仅仅在内存表现正常时有效。“特殊”的内存不行。最常见的“特殊”内存是用来做内存映射I/O的内存。这种内存实际上是与外围设备比如外部传感器或者显示器打印机网络端口通信而不是读写通常的内存比如RAM。这种情况下再次考虑这看起来冗余的代码:
```cpp ```cpp
auto y = x; // read x auto y = x; //读x
y = x; // read x again y = x; //再次读x
``` ```
如果x的值是一个温度传感器上报的第二次对于x的读取就不是多余的因为温度可能在第一次和第二次读取之间变化。 如果`x`的值是一个温度传感器上报的,第二次对于`x`的读取就不是多余的,因为温度可能在第一次和第二次读取之间变化。
类似的,写也是一样 看起来冗余的写操作也类似。比如在这段代码中
```cpp ```cpp
x = 10; x = 10; //写x
x = 20; x = 20; //再次写x
``` ```
如果x与无线电发射器的控制端口关联则代码时控制无线电10和20意味着不同的指令。优化会更改第一条无线电指令 如果`x`与无线电发射器的控制端口关联,则代码是给无线电发指令10和20意味着不同的指令。优化掉第一条赋值会改变发送到无线电的指令流
`volatile`是告诉编译器我们正在处理特殊内存。意味着告诉编译器“不要对这块内存执行任何优化”。所以如果x对应于特殊内存应该声明为`volatile` `volatile`是告诉编译器我们正在处理特殊内存。意味着告诉编译器“不要对这块内存执行任何优化”。所以如果`x`对应于特殊内存,应该声明为`volatile`
```cpp ```cpp
volatile int x; volatile int x;
``` ```
带回我们原始代码 考虑对我们的原始代码序列有何影响
```cpp ```cpp
auto y = x; auto y = x; //读x
y = x; // can't be optimized away y = x; //再次读x不会被优化掉
x = 10; // can't be optimized away x = 10; //写x不会被优化掉
x = 20; x = 20; //再次写x
``` ```
如果x是内存映射或者已经映射到跨进程共享的内存位置等这正是我们想要的。 如果`x`是内存映射(或者已经映射到跨进程共享的内存位置等),这正是我们想要的。
那么在最后一段代码中y是什么类型int还是volatile int? 突击测试!在最后一段代码中,`y`是什么类型:`int`还是`volatile int``y`的类型使用`auto`类型推导,所以使用[Item2](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/1.DeducingTypes/item2.md)中的规则。规则上说非引用非指针类型的声明(就是`y`的情况),`const`和`volatile`限定符被拿掉。`y`的类型因此仅仅是`int`。这意味着对`y`的冗余读取和写入可以被消除。在例子中,编译器必须执行对`y`的初始化和赋值两个语句,因为`x`是`volatile`的,所以第二次对`x`的读取可能会产生一个与第一次不同的值。)
在处理特殊内存时,必须保留看似多余的读取或者无效存储的事实,顺便说明了为什么`std::atomic`不适合这种场景。`std::atomic`类型允许编译器消除此类冗余操作。代码的编写方式与使用`volatile`的方式完全不同,但是如果我们暂时忽略它,只关注编译器执行的操作,则可以说, 在处理特殊内存时,必须保留看似冗余访问和无用存储的事实,顺便说明了为什么`std::atomic`不适合这种场景。编译器被允许消除对`std::atomic`的冗余操作。代码的编写方式与`volatile`那些不那么相同,但是如果我们暂时忽略它,只关注编译器执行的操作,则概念上可以说,编译器看到这个,
```cpp ```cpp
std::atomic<int> x; std::atomic<int> x;
auto y = x; // conceptually read x (see below) auto y = x; //概念上会读x见下
y = x; // conceptually read x again(see below) y = x; //概念上会再次读x见下
x = 10; // write x x = 10; //写x
y = 20; // write x again x = 20; //再次写x
``` ```
原则上,编译器可能会优化为: 会优化为:
```cpp ```cpp
auto y = x; // conceptually read x auto y = x; //概念上会读x见下
x = 20; // write x x = 20; //写x
``` ```
对于特殊内存,显然这是不可接受的。 对于特殊内存,显然这是不可接受的。
现在,就当他没有优化了但是对于x是`std::atomic<int>`类型来说,下面的两条语句都编译不通过。 现在,就像下面所发生的,当`x`是`std::atomic`时,这两条语句都无法编译通过:
```cpp ```cpp
auto y = x; // error auto y = x; //错误
y = x; // error y = x; //错误
``` ```
这是因为`std::atomic`类型的拷贝操作是被删除的(参见[Item11](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/3.MovingToModernCpp/item11.md))。因为有个很好的理由删除。想象一下如果`y`使用`x`来初始化会发生什么。因为`x`是`std::atomic`类型,`y`的类型被推导为`std::atomic`(参见[Item2](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/1.DeducingTypes/item2.md))。我之前说了`std::atomic`最好的特性之一就是所有成员函数都是原子性的,但是为了使从`x`拷贝初始化`y`的过程是原子性的,编译器不得不生成代码,把读取`x`和写入`y`放在一个单独的原子性操作中。硬件通常无法做到这一点,因此`std::atomic`不支持拷贝构造。出于同样的原因,拷贝赋值也被删除了,这也是为什么从`x`赋值给`y`也编译失败。(移动操作在`std::atomic`没有显式声明,因此根据[Item17](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/3.MovingToModernCpp/item17.md)中描述的规则来看,`std::atomic`不支持移动构造和移动赋值)。
可以将`x`的值传递给`y`,但是需要使用`std::atomic`的`load`和`store`成员函数。`load`函数原子性地读取,`store`原子性地写入。要使用`x`初始化`y`,然后将`x`的值放入`y`,代码应该这样写:
这是因为`std::atomic`类型的拷贝操作时被删除的参见Item 11。想象一下如果y使用x来初始化会发生什么。因为x是`std::atomic`类型y的类型被推导为`std::atomic`参见Item 2。我之前说了`std::atomic`最好的特性之一就是所有成员函数都是原子的但是为了执行从x到y的拷贝初始化是原子的编译器不得不生成读取x和写入x为原子的代码。硬件通常无法做到这一点因此`std::atomic`不支持拷贝构造。处于同样的原因拷贝赋值也被delete了这也是为什么从x赋值给y也编译失败。移动操作在`std::atomic`没有显式声明因此对于Item 17中描述的规则来看`std::atomic`既不提移动构造器也不提供移动赋值能力)。
可以将x的值传递给y但是需要使用`std::atomic`的`load和store`成员函数。`load`函数原子读取,`store`原子写入。要使用x初始化y然后将x的值放入y代码应该这样写
```cpp ```cpp
std::atomic<int> y(x.load()); std::atomic<int> y(x.load()); //读x
y.store(x.load()); y.store(x.load()); //再次读x
``` ```
这可以编译,但是可以清楚看到不是整条语句原子,而是读取写入分别原子化执行。 这可以编译,读取`x`(通过`x.load()`)是与初始化或者存储到`y`相独立的函数,这个事实清楚地表明没理由期待上面的任何一个语句会在单独的原子性的操作中整体执行。
给出的代码编译器可以通过存储x的值到寄存器代替读取两次来“优化” 给出上面的代码编译器可以通过存储x的值到寄存器代替读取两次来“优化”
```cpp ```cpp
register = x.load(); // read x into register register = x.load(); //把x读到寄存器
std::atomic<int> y(register); // init y with register value std::atomic<int> y(register); //使用寄存器值初始化y
y.store(register); // store register value into y y.store(register); //把寄存器值存储到y
``` ```
结果如你所见仅读取x一次这是对于特殊内存必须避免的优化(这种优化不允许对`volatile`类型值执行)。 结果如你所见,仅读取`x`一次,这是对于特殊内存必须避免的优化。(这种优化是不允许对`volatile`类型变量执行的。)
事情越辩越明 因此情况很明显
- `std::atomic`用在并发程 - `std::atomic`用在并发程中,对访问特殊内存没用。
- `volatile`用于特殊内存场景 - `volatile`用于访问特殊内存,对并发编程没用。
因为`std::atomic`和`volatile`用于不同的目的,所以可以结合起来使用: 因为`std::atomic`和`volatile`用于不同的目的,所以可以结合起来使用:
```cpp ```cpp
volatile std::atomic<int> vai; // operations on vai are atomic and can't be optimized away volatile std::atomic<int> vai; //对vai的操作是原子性的且不能被优化掉
``` ```
这可以用在比如`vai`变量关联了memory-mapped I/O内存并且用于并发程序的场景 如果`vai`变量关联了内存映射I/O的位置被多个线程并发访问这会很有用
最后一点,一些开发者尤其喜欢使用`std::atomic`的`load`和`store`函数即使不必要时,因为这在代码中显式表明了这个变量不“正常”。强调这一事实并非没有道理。因为访问`std::atomic`确实会更慢一些,我们也看到了`std::atomic`会阻止编译器对代码执行顺序重排。调用`load`和`store`可以帮助识别潜在的可扩展性瓶颈。从正确性的角度来看,没有看到在一个变量上调用`store`来与其他线程进行通信比如flag表示数据的可用性可能意味着该变量在声明时没有使用`std::atomic`。这更多是习惯问题,但是,一定要知道`atomic`和`volatile`的巨大不同。 最后一点,一些开发者在即使不必要时也尤其喜欢使用`std::atomic`的`load`和`store`函数,因为这在代码中显式表明了这个变量不“正常”。强调这一事实并非没有道理。因为访问`std::atomic`确实会比non-`std::atomic`更慢一些,我们也看到了`std::atomic`会阻止编译器对代码执行一些特定的,本应被允许的顺序重排。调用`load`和`store`可以帮助识别潜在的可扩展性瓶颈。从正确性的角度来看,**没有**看到在一个变量上调用`store`来与其他线程进行通信比如用个flag表示数据的可用性可能意味着该变量在声明时本应使用而没有使用`std::atomic`。
这更多是习惯问题,但是,一定要知道`atomic`和`volatile`的巨大不同。
### 必须记住的事 ### 必须记住的事
- `std::atomic`用在不使用锁,来使变量被多个线程访问。是用来编写并发程序的 - `std::atomic`在不使用互斥情况下,来使变量被多个线程访问的情况。是用来编写并发程序的一个工具。
- `volatile`是用在特殊内存的场景中,避免被编译器优化内存 - `volatile`用在读取和写入不应被优化掉的内存上。是用来处理特殊内存的一个工具