标题:浅谈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. HDU 6060 RXD and dividing(LCA)

    [题目链接] http://acm.hdu.edu.cn/showproblem.php?pid=6060 [题目大意] 给一个n个节点的树,要求将2-n号节点分成k部分, 然后将每一部分加上节点1, ...

  2. 【推导】【数学期望】Gym - 101237D - Short Enough Task

    按照回文子串的奇偶分类讨论,分别计算其对答案的贡献,然后奇偶分别进行求和. 推导出来,化简一下……发现奇数也好,偶数也好,都可以拆成一个等比数列求和,以及一个可以错位相减的数列求和. 然后用高中数学知 ...

  3. 【贪心+优先队列】POJ3190-Stall Reservations

    [题目大意] 给出每个奶牛挤奶的时间,同一时间同一畜栏内不会有两头奶牛挤奶,问至少要多少个畜栏. [思路] 将奶牛按照挤奶开始的时间进行升序排序,再用一个小顶堆维护每一个畜栏当前的挤奶结束时间.对于当 ...

  4. JDK源码学习笔记——HashMap

    Java集合的学习先理清数据结构: 一.属性 //哈希桶,存放链表. 长度是2的N次方,或者初始化时为0. transient Node<K,V>[] table; //最大容量 2的30 ...

  5. nginx负载均衡upstream参数配置

    一定要注意两台机器能够telnet 访问通过  如果不能通过则两台机器都执行一下 iptables -F 机器A: php-fpm配置[www]user = wwwgroup = wwwlisten ...

  6. MDD:使用模型驱动开发方式进行快速开发(多图预警)

    相信很多人跟我一样,不喜欢数据展示.列表分页.数据的增.删.改.查,这种简单又烦琐的搬砖活. 所以网上出现很多开源的代码生成工具,在多年前我也写过,根据模版生成简单的View.Action.Servi ...

  7. javacripr基础总结

    js中一切皆为对象 方法=函数 其实也是内建对象 函数都可以用,包括数组,函数 都可以获取 核心dom编程 -- dom d document文档 o 对象 1用户自定义对象, 2内建对象  如Arr ...

  8. easyui combobox设置只读属性

    $("#id").combobox('readonly',true); //只读 $("#id").combobox('readonly',false); // ...

  9. Helm安装和项目使用

    整体架构 1.为什么要用? 首先在原来项目中都是基于yaml文件来进行部署发布的,而目前项目大部分微服务化或者模块化,会分成很多个组件来部署,每个组件可能对应一个deployment.yaml,一个s ...

  10. delphi 自定义内存管理

    1.主要通过GetMemoryManager来hook原来的内存管理. 2.通过SetMemoryManager来设置你自己的新的内存管理,可以用一个内存池来优化和管理程序的内存调用情况. proce ...