volatile: 多线程程序员最好的朋友
volatile
关键字用于阻止编译器进行一些在异步事件代码中进行的可能导致错误的优化。
ByAndrei Alexandrescu
February 01,2001
URL:http://drdobbs.com/cpp/184403766
前言
多线程应用程序非常复杂,很难编写,且易错,调试,跟踪难度大,受到广大用户和编程人员的诟病。然而,凡大而复杂的系统都会用到多线程技术,所以我们还是必须直面应用多线程技术带来的一些问题,特别对于程序员来说,更应如此。
本文主要关注竞态——这也是许多多线程程序出现错误的主要诱因之一。我们将介绍怎样去避免竞态,并让编译器去尽量地避免这些竞态条件的产生。
关键字volatile
虽然C和C++标准中都对线程讨论甚少,但它们引入了关键字volatile来支持多线程编程。跟const
一样,
,volatile
也是一个类型修饰符,它与那些在不同线程中被访问和修改的变量一起使用。
基本上,如果没有关键字volatile
,要么编写多线程程序变得不可能,要么就是编译器浪费了大量优化的机会(做了些不适当的优化)。详细原因解释如下:
考虑下面的代码:
class Gadget
{
public:void Wait(){while (!flag_){Sleep(1000); // sleeps for 1000 milliseconds}}void Wakeup(){flag_ = true;}...
private:bool flag_;
};
Gadget::Wait
的目的就是每秒检测成员变量flag_
是否被另一个线程设为true
。至少程序员是这么想的,但是,Wait
是错误的。
假设编译器认为Sleep(1000)
是一个对外部库的调用,它不可能修改成员变量
flag_
,那么,编译器就会武断地决定它可以缓存
flag_
的值到某个寄存器中并使用寄存器中的值,而不是去访问相对低速的内存。对于单线程代码来说,这是一个极佳的优化,然而,在本例中,它损害了代码的正确性:当你调用
Wait
等待某个
Gadget
对象时,尽管另一个线程调用了
Wakeup
,但是Wait
还是会永远循环。这是因为
flag_
值的变化并没有同步更新到缓存了
flag_
值的寄存器上。这个优化有点过了。
大部分情况下,将变量缓存到寄存器中是一个非常有价值的优化,所以浪费这个优化也有点可惜。不过,
C
和
C++
允许我们显式地禁用这种优化。如果对一个变量使用
volatile
修饰符,那么编译器将不会缓存变量到寄存器中——每次对变量的访问将会从实际内存中去读取变量的值。所以,为了让Gadget
的
Wait
/Wakeup
协同工作,只需要为flag_
添加一个修饰符:
class Gadget
{
public:... as above ...
private:volatile bool flag_;
};
通常这就是关键字volatile
的主要用处,而且一般人在讲这个关键字的用处时也会到此为止。然而,还有更多有关该关键字的一些用处值得我们进一步研究。
用volatile限定用户自定义的类型
关键字volatile
不仅仅是限定基本类型,它还可以限定用户自定义的类型。在这种情况下,
volatile
限定类型的方式与const
类似。
(你也可以对相同类型同时使用关键字const
和
volatile
。)
与const
不同的是
,volatile
区分基本类型和用户自定义类型。即,与类不同,当被
volatile
限定时,基本类型仍然支持它们原来的操作(加、乘、赋值等等。)例如,你可以将一个非
volatile
限定的整型值赋给一个被
volatile
限定的整型变量,但是,你却不能将一个非
volatile
限定的对象赋给一个被
volatile
限定的对象。相关例子如下
class Gadget
{
public:void Foo() volatile;void Bar();...
private:String name_;int state_;
};
...
Gadget regularGadget;
volatile Gadget volatileGadget;
如果你认为volatile
没什么用的话,那么下面的一些结果会让你吃惊。
volatileGadget.Foo(); // ok, volatile fun called for// volatile objectregularGadget.Foo(); // ok, volatile fun called for// non-volatile object
volatileGadget.Bar(); // error! Non-volatile function called for// volatile object!
一个被volatile
限定的类只暴露了它的一部分接口,这些接口在类的实现者的控制之下。用户只需要使用
const_cast
做个类型转换就可以访问类的其他接口。另外,跟被
const
限定的类一样,类的成员也会变得被
volatile
限定了。
(例如,volatileGadget.name_
和volatileGadget.state_
都是
volatile
变量。
).
从一个非限定的类型转化为volatile限定型非常简单,然而,如同const
一样,你不能将
volatile
限定型转换为非
volatile
限定型。
必须使用如下方式:
Gadget& ref = const_cast<Gadget&>(volatileGadget);
ref.Bar(); // ok
volatile, 关键代码区和竞态条件
在多线程程序中,最简单且最常用的同步工具就是互斥量。互斥量向外提供Acquire
和Release
操作原语。一旦在某个线程中调用
Acquire
,任何其他调用
Acquire
的线程将会被阻塞。然后,当该线程调用
Release
,被阻塞的线程中的一个将会获得互斥量而结束阻塞状态。按句话说,对于任意一个互斥量,仅有一个线程在调用Acquire
和Release
之间获得处理器时间
.在Acquire
和Release
调用之间的代码称为关键代码区。
互斥量用于保护竞态条件下的数据。按照定义,当线程的数量超过线程调度时所依赖的数据的数量时,竞态条件就产生了。当两个或更多的线程竞争使用相同的数据时竞态条件就会出现。由于线程可以在任意时刻中断,数据可能会被破坏或错误解析。因此,对数据的改变或访问有时也必须小心地置于关键代码区内。在面向对象编程中,这通常意味着你将一个互斥量作为类的成员变量,并在任何时候访问类的状态时使用它。
说了一大堆,到底跟volatile
有何联系呢?接下来,我们对
C++
的类型世界与线程的语义世界作个对比:
在关键代码区
外
,任何线程可以在任意时刻中断
;
没有任何控制,因此,从多个线程中访问的变量是
volatile
的。这与
volatile
的初衷吻合——阻止编译器无意地将多个线程使用的值缓存。在由互斥量定义的关键代码区
内
,仅有一个线程可以访问。因此,在关键代码区内,执行代码是单线程的。被控制的变量不再是
volatile
的 ——你可以删除volatile
限定符。
简言之,线程间共享的数据在概念上来说,当位于关键代码区外时是volatile
的
,
当位于关键代码区内时,是
non-volatile
的。
当锁定一个互斥量时,就进入了一个关键代码区。可以利用const_cast
来删除
volatile
限定符。当我们将这些操作放在一起,就形成了一个
C++
类型系统与应用程序线程语义之间的一个联系。我们可以让编译器为我们检查竞态条件。
LockingPtr
我们需要一个工具收集互斥量的获取操作以及const_cast
操作。让我们来开发一个
LockingPtr
类模板,你通过一个
被volatile限定的
对象和一个互斥量
mtx
来初始化它。在
LockingPtr
类的生命周期期间,它一直占有
mtx
。同时它提供访问被转化为非
volatile限定
的
obj
。该访问是通过操作符
->
和操作符
*
以智能指针的形式实现的。
const_cast
操作是在类LockingPtr
内部执行的。从语义上来讲,这是合法的,因为类
LockingPtr
在它的整个生命周期内都是拥有互斥量的。
首先,我们来定义一下类Mutex
的基本结构:
class Mutex
{
public:void Acquire();void Release();...
};
为了使用LockingPtr
,你得使用你操作系统的本地数据结构和原语函数来实现
Mutex
。
LockingPtr
是一个类型可控的模板类。例如,如果你想控制一个
Widget
对象,那么可以使用一个变量类型为
volatile Widget
来初始化一个
LockingPtr
<Widget>
实例。
LockingPtr
的定义非常简单。
LockingPtr
实现了一个不是很复杂的智能指针。它主要是用于收集
const_cast
操作和关键代码区。
template <typename T>
class LockingPtr {
public:// Constructors/destructorsLockingPtr(volatile T& obj, Mutex& mtx): pObj_(const_cast<T*>(&obj)),pMtx_(&mtx){ mtx.Lock(); }~LockingPtr(){ pMtx_->Unlock(); }// Pointer behaviorT& operator*(){ return *pObj_; }T* operator->(){ return pObj_; }
private:T* pObj_;Mutex* pMtx_;LockingPtr(const LockingPtr&);LockingPtr& operator=(const LockingPtr&);
};
尽管很简单,LockingPtr
在编写正确的多线程程序时非常有用。你将那些在多线程之间共享的线程对象定义为
volatile
限定型的,对它们进行
const_cast
操作——总是使用
LockingPtr
类型的自动对象。我们用下面的例子来说明一下:
假设你有两个线程,它们之间共享一个vector<char>
对象:
class SyncBuf {
public:void Thread1();void Thread2();
private:typedef vector<char> BufT;volatile BufT buffer_;Mutex mtx_; // controls access to buffer_
};
在一个线程函数中,你只要使用一个LockingPtr<BufT>
实例来获得对buffer_
成员变量的可控访问:
void SyncBuf::Thread1() {LockingPtr<BufT> lpBuf(buffer_, mtx_);BufT::iterator i = lpBuf->begin();for (; i != lpBuf->end(); ++i) {... use *i ...}
}
上述代码简单易懂——无论何时你需要使用buffer_
,你必须创建一个LockingPtr<BufT>
实例指向它。之后,你就可以访问
vector
的所有接口。
代码的好处是如果你代码中有个错误,编译器将会把它指出来:
void SyncBuf::Thread2() {// Error! Cannot access 'begin' for a volatile objectBufT::iterator i = buffer_.begin();// Error! Cannot access 'end' for a volatile objectfor (; i != lpBuf->end(); ++i) {... use *i ...}
}
你不能访问buffer_
的任何函数,直到你应用const_cast
转换或使用
LockingPtr
。不同之处是
LockingPtr
提供了一种有序的方式对
volatile
变量执行
const_cast
转换。
LockingPtr
表达能力相当强。如果你仅需要调用其中的一个函数,你可以创建一个匿名的临时
LockingPtr
对象并直接使用它:
unsigned int SyncBuf::Size() {return LockingPtr<BufT>(buffer_, mtx_)->size();
}
回到基本类型
我们已经看到volatile
是怎样保护对象无序地访问以及
LockingPtr
是如何提供一个简单且有效地方式来编写线程安全的代码。现在让我们回到基本类型,
volatile
对它们的处理不同。
让我们考虑如下一个例子,在该例子中多个线程共享一个int
变量。
class Counter
{
public:...void Increment() { ++ctr_; }void Decrement() { —ctr_; }
private:int ctr_;
};
如果Increment
和
Decrement
是从不同的线程中调用的,上述代码是有问题的。首先,
ctr_
必须是volatile
限定型的。其次,即使是一个看起来像原子操作的
++ctr_
实际上是一个三阶段操作。内存本身没有计算能力。当增加一个变量时,处理器:
读取某个寄存器中的变量值。
递增寄存器中的值。
将结果定回到内存中。
这个三阶段操作称为RMW(Read-Modify-Write)。在一个RMW操作的修改阶段,大部分处理器释放了内存总线以便其他处理器能够访问内存。
如果此时另一个处理器对相同的变量执行了一个RMW操作,此时产生了一个竞态:第二次写操作覆盖了第一次写操作。
为了避免这种情况,你可以再次借助LockingPtr
:
class Counter
{
public:...void Increment() { ++*LockingPtr<int>(ctr_, mtx_); }void Decrement() { —*LockingPtr<int>(ctr_, mtx_); }
private:volatile int ctr_;Mutex mtx_;
};
现在代码是正确的,但是与SyncBuf
的代码相比,质量略差了点,为什么?因为使用
Counter
的话,编译器在你错误地直接访问
ctr_
(没有锁定它)时不会给出任何警告。如果ctr_
是volatile
限定型的,编译器不会报出
++ctr_
有错,尽管产生的代码只是一个简单的错误。编译器不再是你的同盟者,你只有倍加小心才能避免竞态产生。
那你应该怎么做呢?将你使用的基本数据封装在更高级的数据结构中并使用volatile
来限定它。耐人寻味的是,直接使用
volatile
来限定一些内建的数据类型被认为是非常不好的做法,尽管这是
volatile
最原始的初衷.
volatile限定型成员函数
到此为止,我们已经拥有了一些拥有多个volatile
限定型的数据成员的类
;
现在让我们反过来考虑一下设计一些类,这些类会成为一些更大的对象的一部分并在不同的线程之间共享。这时,
volatile
限定型的成员函数就会非常有用。
当设计你自己的类时,你可以将那些线程安全的成员函数设为volatile
限定型的。你必须假设在任何时候在任何代码中调用
volatile
限定型的函数。别忘了:
volatile
等同于自由的多线程代码和非关键代码区
;
非
volatile
限定型等同于单线程情形或是一个关键代码区内。
例如,你可以定义一个类Widget
以两种方式实现一个操作——一个线程安全的和一个快速地非线程安全的。
class Widget
{
public:void Operation() volatile;void Operation();...
private:Mutex mtx_;
};
注意重载的使用。现在Widget
的使用者可以用统一的语法方式调用
Operation
,对
volatile
限定型对象来讲,获得了线程安全性,对一般的对象,获得了速度。
使用者必须小心地将共享的Widget
对象定义为
volatile
限定型的。
当实现一个volatile
限定型的成员函数时,第一个操作通常是用一个
LockingPtr
实例锁定
this
。然后,接下来的工作就是使用非volatile
限定型的同名函数:
void Widget::Operation() volatile
{LockingPtr<Widget> lpThis(*this, mtx_);lpThis->Operation(); // invokes the non-volatile function
}
总结
当编写多线程程序时,你可以使用volatile
。你必须坚持如下原则:
定义所有共享的对象为
volatile
限定型的。
不要对基本类型直接使用
volatile
。
当定义共享类时,使用
volatile
限定型的成员函数来表示线程安全。
如果你这样做了,并且你使用本文介绍的这个简单的通用组件LockingPtr
,你就可以写线程安全代码,且不用过多担心竞态条件,因为编译器会为你关注一些竞态条件并且忠实地指出你出错的地方。
转载于:https://my.oschina.net/fuyajun1983cn/blog/263788
volatile: 多线程程序员最好的朋友相关推荐
- volatile——多线程程序员最好的朋友
volatile修正符及让你的编译器为你检查竞态条件(race conditions) 并不是我故意想弄糟你的心情,但是在这期专栏里,我们将讨论多线程编程这一话题.正如上一期Generic里所说的,编 ...
- 程序员广交四海朋友群
群一:71923869 群二:85907325 程序员俱乐部,是由程序员自发组织的.非营利性质的.民间组织,旨在为周边地区广大程序员.编程爱好者和高校计算机爱好者提供一个融洽而稳定的结交朋友.交流技术 ...
- 女程序员如何在朋友圈报喜-笑的我肚子疼
11. 前后端分离开发. 12. 照着文档一步一步做到了最后一步. 13. 最牛叉的代码 14. 当年学C语言的过程. 15. 测试环境一切ok,马上上线 16.千万别乱动老项目 17. 上年纪的前端 ...
- 程序员的一往情深朋友圈1
1.为你编辑快乐的程序,给你安装如意的系统,帮你解开幸福的密码,与你下载好运的软件,程序员节到了,一切准备就绪,愿你程序员节心情无限好,笑口合不拢. 2.滚滚红尘,物欲横流.灯红酒绿,纸醉金迷.不过世 ...
- “力挺Java!拒绝Python”9万程序员刷爆朋友圈……
那天,被一个应届生小伙伴问到:第一份工作选Java还是Python好? 我可以说"我认为编程语言永远是工具,用的顺手,选啥都行!" "没有最好,只有最适合自己." ...
- 架构之道:3个程序员成就微信朋友圈日均10亿发布量
前言 截止到2015年7月,微信每月活跃用户约5.49亿,朋友圈每天的发表量(包括赞和评论)超过10亿,浏览量超过100亿.得益于4G网络的发展,以上数据仍有很快的增长,而且相对于PC互联网时代,移动 ...
- 写给想当程序员的朋友
谨以此文献给所有想当程序员的朋友 (一) 文章由来及个人经历 我是一名计算机专业的本科毕业生,毕业已经1年多了.毕业后从事的是软件编程工作,经常有其他专业的朋友想从事软件编程工作,向我请教如何,因 ...
- (转)写给想当程序员的朋友
--――一个还不太老的程序员的体会 (初稿) 软件以程序员为本(<程序员>) 谨以此文献给所有想当程序员的朋友 (一) 文章由来及个人经历 我是一名计算机专业的本科毕业生,毕业已经1年多了 ...
- 写给想当程序员的朋友们
谨以此文献给所有想当程序员的朋友 (一) 文章由来及个人经历 我是一名计算机专业的本科毕业生,毕业已经 1 年多了.毕业后从事的是软件编程工作,经常有其他专业的朋友想从事软件编程工作,向我请教如何 ...
最新文章
- 英语 语义分割_Padlex数据处理-语义分割-分段变换,PaddleX,segtransforms
- 05-CA/TA编程:hmac demo
- FTP and Firewalls
- 朗读评价语言集锦_干货 | 教师课堂评价规范用语的几点建议,建议收藏!
- Java 11:字符串类中的新方法
- Python version 3.3 required, which was not found in the registry
- Simulink嵌入式自动代码DSP F28335(4)——SVPWM
- HTTP请求方式和幂等性
- 智慧供应链的学习笔记(库存管理、配补货、仓间调拨、控制塔等)
- 编译原理常用简称或英文原称(思维导图形式)
- 洛谷P1308统计单词数C语言
- 国外14部经典励志电影推荐
- ARM Cortex-M 调试器 - 基础知识
- 【沐风老师】3DMAX一键生成圣诞树建模插件使用教程
- “知识共享”扎根中国,前景无量
- python在地图上标注点_怎样用python画地图上的标注线
- TCP/IP四层模型、HTTP、HTTPS、TCP
- Linux 系统注册系统服务流程
- uni-app入门:wxs基本使用
- 基于LSTM的美国大选的新闻真假分类【NLP 新年开胃菜】