标题:浅谈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. [Codeforces-div.1 55D] Beautiful numbers

    [Codeforces-div.1 55D] Beautiful numbers 试题分析 还是离散化...\(f_{i,j,k}\)表示i位,gcd为j,余数为k. #include<iost ...

  2. 【斜率优化】Average

    [UVa1451]Average 算法竞赛入门经典第8章8-9 ( P243 ) 题目大意:给定一个长度为N的01串,选择一个长度至少为L的连续子串,使序列平均值最大 (N<=100001) 题 ...

  3. BZOJ 2242 [SDOI2011]计算器(快速幂+Exgcd+BSGS)

    [题目链接] http://www.lydsy.com/JudgeOnline/problem.php?id=2242 [题目大意] 给出T和K 对于K=1,计算 Y^Z Mod P 的值 对于K=2 ...

  4. 【欧拉回路】【欧拉路径】【Fleury算法】CDOJ1634 记得小苹初见,两重心字罗衣

    Fleury算法看这里 http://hihocoder.com/problemset/problem/1181 把每个点看成边,每个横纵坐标看成一个点,得到一个无向图. 如果新图中每个点的度都是偶数 ...

  5. 零起点学算法09——继续练习简单的输入和计算(a-b)

    #include<stdio.h> int main() { int a,b; scanf("%d %d",&a,&b); printf("% ...

  6. WPF中的动画——(五)路径动画

    路径动画是一种专门用于将对象按照指定的Path移动的动画,虽然我们也可以通过控制动画的旋转和偏移实现对象的移动,但路径动画更专业,它的实现更加简洁明了. 路径动画中最常用的是MatrixAnimati ...

  7. Visual Studio Image Library现在带矢量图标了

    Visual Studio Image Library是微软提供的一套不可多得的高质量图标库(Visual Studio中自己使用的),我在自己写的一些小程序中一直有用到它们.今天天想把之前的程序中的 ...

  8. [转] Moran's I

    李旭, Matlab: Moran's I原文地址 Introduction In statistics, Moran's I is a measure of spatial autocorrelat ...

  9. java使用HttpClient 发送get、pot请求

    package eidolon.messageback.PostUtil; import java.io.BufferedReader; import java.io.IOException; imp ...

  10. js中几种常见的方法的实例 shift,unshift,push,prop

    1.shift()定义和用法 shift() 方法用于把数组的第一个元素从其中删除,并返回第一个元素的值. 语法:arrayObject.shift() 返回值:数组原来的第一个元素的值. 说明:如果 ...