标题:浅谈Volatile与多线程
2011-04-19 22:49:17
最近看的比较杂,摘了一些人的笔记!
随着多核的日益普及,越来越多的程序将通过多线程并行化的方式来提升性能。然而,编写正确的多线程程序一直是一件非常困的事情,volatile关键字的使用就是其中一个典型的例子。
C/C++中的volatile一般不能用于多线程同步
在C/C++中,如果想把一个变量声明为volatile,就相当于告诉编译器这个变量是“易变的”,他随时可能在其他地方被修改,所以编译器不能对其做任何变化:即每次读写该变量时都必须对其内存地址直接进行操作,并且所以对该变量的操作都必须严格按照程序中规定的顺序执行。举例来说,编译器的常常做的一种性能优化就是把需频繁读取的变量缓存到寄存器中,以提升访问速度。但如果该变量的值随时可能在片外被改变的话,那么就有可能出现被缓存的值并不是该变量的最新值情况,从而出现运行错误。在这种情况就需要用volatile关键字来修饰这个变量,以确保编译器不会对该变量读写操作进行任何缓存优化。另一个例子就是内存映射I/O操作。如下代码所示:
Int *p = get_io_address();
Int a, b;
A = *p;
B = *p;
P是一个指向硬件I/O端口的指针,该端口的值在每进行一次读操作后都会变化。这个程序连续对该端口进行两次读取操作已将两个不同的值分别赋值给a和b。如果不把a和b声明为volatile的话,编译器可能会”自作聪明”地认为两次从p读取的值都是一样的,从而把*b=*p优化成b = a,最终导致程序出错。
虽然C/C++中volatile关键字对这种“易变“的读写操作能起到一定的保护,但他却并不适用于多线程程序中共享变量的同步操作。究其根源,就在于C/C++标准中并没有volatile赋予原子性和顺序性的语义。

原子性
下面举个例子说明原子性。i++这看似原子的语句其实有三个操作组成:将该值从内存地址读取到寄存器中,对寄存器中的值进行加1操作,最后再将新值写回内存中,正是因为i++并不是原子的,所以如果两个线程同时进行i++操作的话仍会产生数据竞跑,从而导致i的最终值不等于2.在这种情况下,C/C++中的volatile关键字根本无法对该操作的原子性提供任何保障。
Volatile int  i=0;
//线程1
I++;
//线程2
I++;
顺序性
不幸的是,现在C/C++标准中的volatile关键字对共享变量操作的顺序性也未提供任何保障。以本文中的dekker算法为例:当两个线程分别执行dekker1和dekker2函数时候,改程序通过对flag1/2和turn的读写来实现两个线程对临界区中共享变量gCounter的互斥访问。这个算法的关键就在于对flag1/2和turn的读写操作是在其写操作之后进行的,因此它能保证dekker1和dekker2中对gCounterde的操作时互斥的,相当于把gCounter++放到一个临界区中去了。Dekker算法如下所示:
Volatile int flag1 = 0;
Volatile int flag2 = 0;
Volatile int turn = 1;
Volatile int gCounter = 0;
Void dekker1()
{
       Flag1 = 1;
       Turn = 2;
While( (flag2 == 1) && ( turn == 2) ){}
//进入临界区
       gCounter++;
       flag1 = 0; //离开临界区
}
 
Void dekker2()
{
       Flag2 = 1;
       Turn = 2;
While( (flag1 == 1) && ( turn == 2) ){}
//进入临界区
       gCounter++;
       flag2 = 0; //离开临界区
}
 
尽管volatile规定编译器不能对同一变量的所有操作进行乱序优化,但它却不能阻止编译器对不同volatile变量间的操作进行乱序优化。例如,编译器可能把dekker1中的flag2读操作提到flag1和turn写操作之前,从而导致对临界区的互斥访问失效,最终gCounter++操作就会出现数据竞跑现象。事实上,即使编译器没有对这个程序做任何优化,volatile 关键字也不能阻止多核CPU对该程序的乱序优化。以常见的x86硬件来说,它可以对不同变量x,y的store x --àload y进行乱序优化,把load y操作提到store x操作之前。这样的话,dekker1中flag2的读操作还是有可能会被提到flag1和turn的写操作之前,最终导致错误的计算结果。
那为什么编译器和多核CPU会对多线程程序做这样的乱序优化呢?因为从单核的视角来看,flag1 和 flag2,turn的读写操作之间没有任何依赖关系的,使用编译器/CPU当然可以对他们进行乱序优化以隐藏一部分的内存访问延迟,从而更好的利用CPU里的流水线。换句话说,这样的优化虽从单线程的角度来讲没有错,但却违反了设计这个多线程算法时所期望的多线程语义。要是解决这个问题,我们需要解决这个问题,我们需要自己添加内存栅栏以显式保证顺序性,或者干脆去别去实现这样的算法,转而使用类似pthread_mutex_lock这样的加锁操作来实现互斥访问。
综合上述,由于现有的C/C++标准中并没有对volatile添加原子性和顺序性的语义,所以绝大部分C/C++程序中使用volatile来进行多线程同步的用法是错误的。其实,我们之所以想用volatile变量进行同步,无非是因为锁,条件变量等方式的开销太大,所以想有一种轻量级的,高效的同步机制。

浅谈Volatile与多线程的更多相关文章

  1. 浅谈volatile关键字

    volatile是一种轻量级的同步机制.它可以保证内存可见性以及防止指令重排序,但是不保证原子性 volatile和JMM机制是不可分割的,在谈volatile的时候有必要先了解以下JMM JMM(J ...

  2. 浅谈 volatile 的实现原理

    在并发编程中我们一般都会遇到这三个基本概念:原子性.可见性.有序性.我们稍微看下volatile 原子性 原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行. ...

  3. 浅谈volatile与automicInteger

    在并发环境中有三个因素需要慎重考量,原子性.可见性.有序性.   voatile 保证了有序性(防止指令冲排序)和变量的内存可见性(每次都强制取主存数据),每次取到volatile变量一定是最新的  ...

  4. 【转】浅谈多核CPU、多线程、多进程

    浅谈多核CPU.多线程.多进程 1.CPU发展趋势 核心数目依旧会越来越多,依据摩尔定律,由于单个核心性能提升有着严重的瓶颈问题,普通的桌面PC有望在2017年末2018年初达到24核心(或者16核3 ...

  5. 浅谈原子操作、volatile、CPU执行顺序

    浅谈原子操作.volatile.CPU执行顺序 在计算机发展的鸿蒙年代,程序都是顺序执行,编译器也只是简单地翻译指令,随着硬件和软件的飞速增长,原来的工具和硬件渐渐地力不从心,也逐渐涌现出各路大神在原 ...

  6. 浅谈iOS多线程

    浅谈iOS多线程 首先,先看看进程和线程的概念. 图1.1 这一块不难理解,重点点下他们的几个重要区别: 1,地址空间和资源:进程可以申请和拥有系统资源,线程不行.资源进程间相互独立,同一进程的各线程 ...

  7. 浅谈C++11中的多线程(三)

    摘要 本篇文章围绕以下几个问题展开: 进程和线程的区别 何为并发?C++中如何解决并发问题?C++中多线程的基本操作 浅谈C++11中的多线程(一) - 唯有自己强大 - 博客园 (cnblogs.c ...

  8. 浅谈C++11中的多线程(二)

    摘要 本篇文章围绕以下几个问题展开: 进程和线程的区别 何为并发?C++中如何解决并发问题?C++中多线程的基本操作 浅谈C++11中的多线程(一) - 唯有自己强大 - 博客园 (cnblogs.c ...

  9. 浅谈C#更改令牌ChangeToken

    前言 在上篇文章浅谈C#取消令牌CancellationTokenSource一文中我们讲解了CancellationTokenSource,它的主要功能就是分发一个令牌,当我取消令牌我可以进行一些回 ...

随机推荐

  1. 【BZOJ 2809】2809: [Apio2012]dispatching (左偏树)

    2809: [Apio2012]dispatching Description 在一个忍者的帮派里,一些忍者们被选中派遣给顾客,然后依据自己的工作获取报偿.在这个帮派里,有一名忍者被称之为 Maste ...

  2. [UOJ55]紫荆花之恋

    第一次打“真正的”动态点分 如果树是静态的,直接点分:用$d_x$代表$x$到分治中心的距离,限制条件即为$d_i+d_j\leq r_i+r_j$,考虑枚举$j$,那么我们要查询有多少满足$d_i- ...

  3. 浙南联合训练赛 D - Broken Clock

    You are given a broken clock. You know, that it is supposed to show time in 12- or 24-hours HH:MM fo ...

  4. Java高级架构师(一)第37节:反向代理和动静分离的实现

    http协议->server->location 输入http:locahost:80/ 进入百度的界面. 做负载转发测试.路径已经转发,可能一些协议已经屏蔽了,导致内容出不来. 关于负载 ...

  5. php-scandir()报错

    l       linux下 vim /usr/local/php/etc/php.in l       直接斜杠找 /disable_functions   回车 l       按i键 l     ...

  6. 自定义Cell引发的悲剧。。。。

    这两天毕设,昨晚微调自定义的cell,之前用mrc的时候直接打开xib布局好自定义cell就行乐,不用去勾选Use Autolayout,可是昨晚开始,自定义cell中,label不能动态调高的问题, ...

  7. 通过pl/sql计算程序的运行时间

    在sqlplus中运行sql语句或者pl/sql的时候如果需要统计运行的时间,只需要开启set timing on选项即可. SQL> set timing onSQL>SQL> s ...

  8. Thunderbird for Ubuntu

    转自:http://www.cnblogs.com/slave_wc/archive/2011/05/02/2034529.html   装好ubuntu 的一般基本配置见本博客另一篇文章: Ubun ...

  9. UITabBarControlller 和 UINavigationController

  10. [Linux] 守护进程和守护线程

    对于JAVA而言,一般一个应用程序只有一个进程——JVM.除非在代码里面另外派生或者开启了新进程. 而线程,当然是由进程开启的.当开启该线程的进程离开时,线程也就不复存在了. 所以,对于JAVA而言, ...