Item40 Use std::atomic for concurrency, volatile for specific memory

原创 2017年05月05日 22:47:59

   volatile关键字在C++中很少被使用,更准确来说是很少被正确使用,它的用途令人很迷惑,甚至这个关键字都不会出现在并发章节。因为这个关键字对于并发编程来说没有任何用处,但是在其他编程语言中这个关键字的用途却很大,因此很值得在本文去探讨一下volatile关键字,排除读者们对volatile关键字的困扰。

推荐一下何登成大神的一篇关于volatile关键字的博客C/C++ Volatile关键词深度剖析

   在C++11中提供了一个std::atomic类模版,可以具体实例化出int、bool、指针等类型实例,这个实例保证了操作的原子性,可以被其他线程查看到操作后的结果。就好比是对操作进行了加锁,但是性能损耗更小,因为其内部使用了一种特殊的机器指令实现,该模版类的基本使用如下:

std::atomic<int> ai(0);
ai = 10;            // 原子的设置ai的值为10
std::cout << ai;    // 原子的读取ai的值,但是std::cout输出的动作并不是原子的
++ai;   // 原子的,递增到11
--ai;   // 原子的,递减到10

   在执行上面的这些操作时,其他线程在任何时间都可以看到ai的最新值,可能是0、10、11不会看到其他的中间值。上面的这段代码中有两点值得细究,第一个就是std::cout << ai,这个语句本身并不是原子的,读取ai的值,这本身是原子的,但是将ai的值输出则不是原子的,在输出的时候其他线程可以改变ai的值。第二个方面是++ai--ai这两个操作,这两个是操作是RMW(Read-Modtify-Write)类型的操作,这也是原子的,这得益于std::atomic类所提供的特性。

​   volatile则相反,使用volatile修饰的变量其操作并不是原子的,其他线程可能会读取到中间值,volatile的基本使用如下:

volatile int vi(0);
vi = 10;
std::cout << vi;
++vi;
--vi;

​   在上面的代码执行过程中,其他线程会去读vi的值时可能会出现任意值,这是一种未定义行为。 为了更一步分析std::atomicvolatile两者行为的不同,下面举一个具体的例子:

std::atomic<int> ac(0);
volatile int vc(0);

有两个线程同时执行下面两个操作:

// 线程1
++ac;
++vc

// 线程2
++ac;
++vc;

​   当两个线程执行完成后,ac的值肯定是2,而vc的值则不一定,volatile不保证vc的最后值是2,它可能是0,也有可能是1,下面让我们来具体分析一下:

  1. 线程1读取vc的值,是0
  2. 线程2读取vc的值,仍然是0
  3. 线程1增加读取vc的值为为0,然后递增,最后将递增后的值写入到vc
  4. 线程1增加读取vc的值为为0,然后递增,最后将递增后的值写入到vc

   上面这种情况,vc的值最后是1,进行了两次递增,但是递增的结果是想同的,因为线程1和2看到了vc的值是想同的,对于这种行为,我们称之为存在data race(数据竞争),是一个未定义的行为。需要使用mutex,或者是原子操作来避免这种未定义行为的出现。

   RWN这种操作的原子性并不是std::atomicvolatile两者的唯一一个区别,考虑另外一个场景,当一个线程完成一个重要计算后,通知另外一个线程,很明显这个场景很适合我们在Item39中提到的方案来解决,不过在这里使用std::atomic来解决这件事,通过使用std::atomic<bool>作为一个flag进行通知,部分代码如下:

std::atomic<bool> valAvailabel(false);
auto imptValue = computeImportantValue();
valAvailabel = true;

   一眼看上去,valAvailabel的赋值是在imptValue赋值之前发生的,但是实际上并不一定是这样的,编译器可能会对这两个赋值语句进行重排序,即使编译器没有做这样的工作,硬件也可能会对这两个操作进行指令级别的重排,对于给定如下的顺序:

a = b;
x = y;

   因为a,b和x,y互相不产生依赖,所以编译器可能会进行重排,重排后的顺序如下:

x = y;
a = b;

   这种重排序的目的是为了运行的更快,无论是在编译器层面的重排序,还是在CPU指令集层面的重排,然后这一切都被std::atomic屏蔽了,默认情况下std::atomic禁止了底层编译器和硬件的重排序。这种行为称为顺序一致性模型,std::atomic也支持更加复杂的内存模型,比如松散模型,这种模型下可以是的代码运行的更快。相反的是volatile无法阻止这种重排序的发生。综上所述,volatile存在两个问题,第一个就是原子性,第二个就是重排序的问题。这也就解释了为何volatile在并发编程领域中几乎没有任何价值。

   既然volatile在并发编程领域几乎没有任务价值,那么volatile存在的意义是什么呢?首先我们来看下面这段代码:

int x = 10;
auto y = x;
std::cout << x;

   上面的代码中,多次读取x的值,编译器为了优化会将x的值放在寄存器中,每次后面读取x的值时,直接从寄存器返回即可。同理对于多次写一个内存位置的情况,编译器也会做优化,代码如下:

x = 10;
x = 12;

   编译器会进行优化,实际上只执行了x = 12这次操作,省略了x = 10这一步。这些优化加速了程序的运行速度,但是如果是在一些特殊的设备上进行这样的操作就会导致不符合预期的效果,我们都知道一些外部设备的访问其实是可以通过访问内存的形式来访问,对于这些设备来说每一次访问都会让设备产生一定的效果,是不能省略掉的。就好比x = 10; x = 12来说,对于某些设备来说这可能是一个渐变的效果,如果省去了x = 10那么这个效果就大打折扣了。 为此对于这种情况来说必需使用volatile来告诉编译器禁止对变量的读写进行优化。std::atomic无法做到这一点,它只保证了操作的原子性,编译器仍然会多次冗余的读写操作进行优化。

版权声明:本文为博主原创文章,未经博主允许不得转载。 举报

相关文章推荐

Item19 Use std::shared_ptr for shared-ownership resource management

这个系列的文章来自于Effective Modern C++的读书笔记,我抽取了其中比较重要的,不容易理解的,平常我们开发过程中也不太在意的一些Item进行分析。 ​ 在上一篇文章中讨论了std:...

Item18 Use std::unique_ptr for exclusive-ownership resource management

这个系列的文章来自于Effective Modern C++的读书笔记,我抽取了其中比较重要的,不容易理解的,平常我们开发过程中也不太在意的一些Item进行分析。 ​ 在这篇文章中我主要探讨下st...

我是如何成为一名python大咖的?

人生苦短,都说必须python,那么我分享下我是如何从小白成为Python资深开发者的吧。2014年我大学刚毕业..

条款40:将std::atomic用于并发,而volatile用于专有内存

条款40:将std::atmic用于并发,而volatile用于专有内存声明:本文翻译自《Effective Modern C++》,自己边看边翻译的,不保证与英文原版完全字字对应,纯粹以学习为目的,...

Item20 Use std::weak_ptr for std::shared_ptr like pointers that can dangle

这个系列的文章来自于Effective Modern C++的读书笔记,我抽取了其中比较重要的,不容易理解的,平常我们开发过程中也不太在意的一些Item进行分析。 ​ 原始指针有一个致命的问题就是...

Built-in functions for atomic memory access

在linux2.6.18之后,删除了和头文件,编译器提供内建(built-in)原子操作函数。需要在gcc编译选项中指明CPU类型。如gcc -marth=i686 -o hello hello.c。...

#("The POSIX name for this item is deprecated. Instead, use the ISO C++ conformant name:")

在使用visual studio 2015 写程序时 ,出现了(“The POSIX name for this item is deprecated. Instead, use the ISO C...

Effective Objective-C 2.0:Item 50: Use NSCache Instead of NSDictionary for Caches

Item 50: Use NSCache Instead of NSDictionary for Caches A common problem encountered when develop...

Effective Objective-C 2.0: Item 45: Use dispatch_once for Thread-Safe Single-Time Code Execution

Item 45: Use dispatch_once for Thread-Safe Single-Time Code Execution The Singleton design pattern—...

Effective Objective-C 2.0: Item 35: Use Zombies to Help Debug Memory-Management Problems

Item 35: Use Zombies to Help Debug Memory-Management Problems Debugging memory-management issues ...

OpenBSD Tips: Enable 'sudo' command for a specific user

环境:OpenBSD 5.3 默认安装OpenBSD时,创建一个普通用户:yapingxin。 当使用该普通用户执行“sudo”命令时,提示: yapingxin is not in the su...
返回顶部
收藏助手
不良信息举报
您举报文章:深度学习:神经网络中的前向传播和反向传播算法推导
举报原因:
原因补充:

(最多只允许输入30个字)