volatile

volatile的介绍

volatile的主要作用是:提示编译器该对象的值有可能在编译器未监測的情况下被改变。   

volatile类似于大家所熟知的const也是一个类型修饰符。volatile是给编译器的指示来说明对它所修饰的对象不应该运行优化。volatile的作用就是用来进行多线程编程。在单线程中那就是仅仅能起到限制编译器优化的作用。所以单线程的童鞋们就不用浪费精力看以下的了。

没有volatile的结果

      假设没有volatile,你将无法在多线程中并行使用到基本变量。以下举一个我开发项目的实例(这个实例採用的是C#语言但最好还是碍我们讨论C++)。在学校的一个.Net项目的开发中,我以前在多线程监控中用到过一个基本变量Int32型的,我用它来控制多线程中监控的一个条件。考虑到基本变量是编译器自带的并且无法用lock锁上,我想当然的以为是原子操作不会有多线程的问题,可实际执行后发现程序的执行有时正常有时异常,改为用Dictionary对象处理并加锁以后才彻底正常。如今想来应该是多线程同一时候操作该变量了,详细的将在以下说清。

volatile的作用

      假设一个基本变量被volatile修饰,编译器将不会把它保存到寄存器中,而是每一次都去訪问内存中实际保存该变量的位置上。这一点就避免了没有volatile修饰的变量在多线程的读写中所产生的因为编译器优化所导致的灾难性问题。所以多线程中必需要共享的基本变量一定要加上volatile修饰符。当然了,volatile还能让你在编译时期捕捉到非线程安全的代码。我在以下还会介绍一位大牛使用智能指针来顺序化共享区代码的方法,在此对其表示感谢。

      泛型编程中以前说过编写异常安全的代码是非常困难的,但是相比起多线程编程的困难来说这就太小儿科了。多线程编程中你须要证明它正确,须要去重复地枯燥地调试并修复,当然了,资源竞争也是必须注意的,最可恨的是,有时候编译器也会给你点颜色看看。。。

class Student { public: void Wait() //在北航排队等吃饭实在是非常痛苦的事情。。。 { while (!flag) { Sleep(1000); // sleeps for 1000 milliseconds } } void eat() { flag = true; } ... private: bool flag; };

      好吧,多线程中你就等着吃饭吧,可在这个地方预计你是永远等不到了,由于flag被编译器放到寄存器中去了,哪怕在你前面的那位童鞋告诉你flag=true了,可你就好像瞎了眼看不到这些了。这么诡异的情况的发生时由于你所用到的推断值是之前保存到寄存器中的,这样原来的地址上的flag值更改了你也没有获取。该怎么办呢?对了,改成volatile就攻克了。

      volatile对基本类型和对用户自己定义类型的使用与const有差别,比方你能够把基本类型的non-volatile赋值给volatile,但不能把用户自己定义类型的non-volatile赋值给volatile,而const都是能够的。另一个差别就是编译器自己主动合成的复制控制不适用于volatile对象,由于合成的复制控制成员接收const形參,而这些形參又是对类类型的const引用,可是不能将volatile对象传递给普通引用或const引用。

怎样在多线程中使用好volatile

      在多线程中,我们能够利用锁的机制来保护好资源临界区。在临界区的外面操作共享变量则须要volatile,在临界区的里面则non-volatile了。我们须要一个工具类LockingPtr来保存mutex的採集和volatile的利用const_cast的转换(通过const_cast来进行volatile的转换)。

      首先我们声明一个LockingPtr中要用到的Mutex类的框架:

class Mutex { public: void Acquire(); void Release(); ... };

      接着声明最重要的LockingPtr模板类:

template <typename T> class LockingPtr { public: // Constructors/destructors LockingPtr(volatile T& obj, Mutex& mtx) : pObj_(const_cast<T*>(&obj)), pMtx_(&mtx) { mtx.Lock(); } ~LockingPtr() { pMtx_->Unlock(); } // Pointer behavior T& operator*() { return *pObj_; } T* operator->() { return pObj_; } private: T* pObj_; Mutex* pMtx_; LockingPtr(const LockingPtr&); LockingPtr& operator=(const LockingPtr&); };

      虽然这个类看起来简单,可是它在编写争取的多线程程序中很的实用。你能够通过对它的使用来使得对多线程中共享的对象的操作就好像对volatile修饰的基本变量一样简单并且从不会使用到const_cast。以下来给一个样例:

       如果有两个线程共享一个vector<char>对象:

class SyncBuf { public: void Thread1(); void Thread2(); private: typedef vector<char> BufT; volatile BufT buffer_; Mutex mtx_; // controls access to buffer_ };

      在函数Thread1中,你通过lockingPtr<BufT>来控制訪问buffer_成员变量:

void SyncBuf::Thread1() { LockingPtr<BufT> lpBuf(buffer_, mtx_); BufT::iterator i = lpBuf->begin(); for (; i != lpBuf->end(); ++i) { ... use *i ... } }

      这个代码非常easy编写和理解。仅仅要你须要用到buffer_你必须创建一个lockingPtr<BufT>指针来指向它,并且一旦你这么做了,你就获得了容器vector的整个接口。并且你一旦犯错,编译器就会指出来:

void SyncBuf::Thread2() { // Error! Cannot access 'begin' for a volatile object BufT::iterator i = buffer_.begin(); // Error! Cannot access 'end' for a volatile object for (; i != lpBuf->end(); ++i) { ... use *i ... } }

      这种话你就仅仅有通过const_cast或LockingPtr来訪问成员函数和变量了。这两个方法的不同之处在于后者提供了顺序的方法来实现而前者是通过转换为volatile来实现。LockingPtr是相当好理解的,假设你须要调用一个函数,你就创建一个未命名的临时的LockingPtr对象并直接使用:

unsigned int SyncBuf::Size() { return LockingPtr<BufT>(buffer_, mtx_)->size(); }

LockingPtr在基本类型中的使用

      在上面我们分别介绍了使用volatile来保护对象的意外訪问和使用LockingPtr来提供简单高效的多线程代码。如今来讨论比較常见的多线程处理共享基本类型的一种情况:

class Counter { public: ... void Increment() { ++ctr_; } void Decrement() { —-ctr_; } private: int ctr_; };

      这个时候可能大家都能看出来问题所在了。1.ctr_须要是volatile型。2.即便是++ctr_或--ctr_,这在处理中仍是须要三个原子操作的(Read-Modify-Write)。基于上述两点,这个类在多线程中会有问题。如今我们就来利用LockingPtr来解决:

class Counter { public: ... void Increment() { ++*LockingPtr<int>(ctr_, mtx_); } void Decrement() { —?*LockingPtr<int>(ctr_, mtx_); } private: volatile int ctr_; Mutex mtx_; };

volatile成员函数

      关于类的话,首先假设类是volatile则里面的成员都是volatile的。其次要将成员函数声明为volatile则同const一样在函数最后声明就可以。当你设计一个类的时候,你声明的那些volatile成员函数是线程安全的,所以那些随时可能被调用的函数应该声明为volatile。考虑到volatile等于线程安全代码和非临界区;non-volatile等于单线程场景和在临界区之中。我们能够利用这个做一个函数的volatile的重载来在线程安全和速度优先中做一个取舍。详细的实现此处就略去了。

总结

      在编写多线程程序中使用volatile的关键四点:

      1.将全部的共享对象声明为volatile;

      2.不要将volatile直接作用于基本类型;

      3.当定义了共享类的时候,用volatile成员函数来保证线程安全;

      4.多多理解和使用volatile和LockingPtr!(强烈建议)

原文地址:https://www.cnblogs.com/gcczhongduan/p/4296710.html