泛型:volatile——多线程程式员最好的朋友vola…

2008-02-23 05:40:50来源:互联网 阅读 ()

新老客户大回馈,云服务器低至5折

我不想破坏您的情绪,但这篇专栏针对多线程编程中最可怕的问题。假如说——正如前面一篇泛型<编程>所说的——写出意外安全(exception-safe)的程式很难,但写意外安全的程式和多线程编程比起来就是小孩子的玩意。
用到多线程的程式是众所周知地难写,难验证,难调试,难维护,总的来说难以驾御。不正确的多线程程式可能会运行几年都不出问题,但在某些时间条件符合时就会导致不可预料的灾难。
不用说,一个写多线程代码的程式员需要一切能得到的帮助。这篇专栏集中讨论竞态条件——在多线程程式中普遍的问题来源——让您了解如何避免他并提供给您工具,而且会让您惊喜地看到您能够让编译器积极地帮助您处理这个问题。

只是个小小的关键字
尽管C和C 标准都明显地对线程保持沉默,他们还是对多线程做了小小的让步,这种让步表现为volatile关键字。
正如他的更为人所知的伙伴const, volatile是个类型修正符(type modifier)。。他的作用是和变量连用使变量能被不同线程访问和修改。根本上说,假如没有volatile的话,要么不可能写出多线程程式,要么编译器浪费极大的优化机会。现在来解释为什么会是这种情况。
考虑下面代码:

class Gadget
{
public:
void Wait ()
{
while (!flag_)
{
Sleep(1000); //睡眠1000毫秒
}
}
void Wakeup ()
{
flag_ = true;
}
...
private:
bool flag_;
};

上面Gadget::Wait的作用是每秒检查一次flag_成员变量,假如那个变量被其他线程设为true时返回。至少这是程式员的本来意图,但,唉,Wait函数是错误的。
假如编译器断定Sleep(1000)是对外部库的一个调用,而且这个调用不可能修改成员变量flag_。那么编译器会决定在寄存器中缓存flag_并且用那个寄存器替代较慢的内存。这对单线程代码来说是很好的优化,但在现在这个情况下,这个优化破坏了正确性:您对某个Gadget对象调用Wait后,尽管另一个线程调用了Wakeup,Wait还会永远循环下去。这是因为对flag_的修改不会反映到缓存flag_的寄存器。这个优化实在是......过度优化了。
把变量缓存到寄存器中在大多数时候是一项很有用的优化,浪费掉就太可惜了。C和C 给您机会来显式禁用这个优化。假如您用volatile标识一个变量,编译器就不会把那个变量缓存到积存器中——对变量的每次访问都直接通过实际内存的位置。所以要让Gadget的Wait/Wakeup正常工作只要正确修饰flag_

class Gadget
{
public:
...同上...
private:
volatile bool flag_;
};

大多数对volatile用途和用法的解释到此为止,并且建议您在多线程中对基本类型加volatile标识符。但是,用volatile您能够做更多事情,因为他是C 奇妙的类型系统的一部分。

对用户定义类型使用volatile
您不单单能够在基本类型前加volatile标识符,而且也能在用户定义类型前加。在这种情况下。volatile象const相同修改这个类型(您也能够同时对同一个类型加const和volatile)
但是不象const,volatile对基本类型和用户定义类型作用不同。就是说,不象类,基本类型加了volatile标识符后仍旧支持他们任何的操作(加,乘,赋值,等等。)。比如,您能够把一个非volatile int赋给一个volatile int,但您不能把一个非volatile对象赋给一个volatile对象。
我们来举例说明volatile怎样作用于用户定义类型。

Class Gadge
{
public:
void Foo() volatile;
void Bar();
...
private:
String name_;
Int state_;
};
...
Gadget regularGadget;
Volatile Gadget volatileGadget;

假如您认为volatile对对象不起作用,那准备好被吓一跳吧。

volatileGadget.Foo(); //成功,对volatile对象调用volatile函数没有问题
regularGadget.Foo(); //成功,对非volatile对象调用volatile函数没有问题
volatileGadget.Bar(); //失败!不能对volatile对象调用非volatile函数

把无标识类型转换为对应的volatile对象很简单。然而,您不能把volatile转回无标识。您必须用cast:

Gadget& ref = const_cast(volatileGadget);
Ref.Bar(); //成功

一个有volatile标识符的类只能访问他接口的子集,一个由类的实现者来控制的子集。用户只能用const_cast来获得对类型接口的完全访问。此外,就象const,volatile会从类传递到他的成员(比如,volatileGadget.name_和volatileGadget.state_是volatile变量)

volatile,临界区(Critical Sections),和竞态条件(Race Conditions)
多线程程式里最简单的也是用得最多得同步设施是mutex,一个mutext提供Acquire和Release基本功能。一旦您在某个线程中调用Acquire,任何其他调用Acquire得线程会被堵塞。稍后当那个线程调用Release,正好会有一个先前被Acquire堵塞的线程被释放。换句话说,有了一个mutex,只有一个线程能够在Acquire调用和Release调用之间得到处理器时间。在Acquire调用和Release调用之间的执行代码本身就是个临界区。(Windows术语有点让人迷惑,因为他把mutex本身叫做一个critical section(临界区)。尽管"mutext"实际上是个进程范围内mutex,但把他们叫做线程mutex和进程mutx会更好些。)
mutex是用来保护数据,防范竟态条件的。根据定义,当多线程对数据处理的结果由线程如何被调度决定时,一个竟态条件产生。当二个或以上的线程竞争使用同样数据时竟态条件出现。因为线程可能在任意时间点被中断,正被处理的数据可能被破坏或被误判。结果是,对数据的修改变作或有时候时读取动作必须用临界区仔细保护起来。在面向对象编程中,这通常意味着您在一个类里存放一个mutex作为成员变量,当您在存取类的数据时使用他。
有经验的多线程程式员在阅读上面两段时可能已在打哈欠了,但那两段的目的是提供一个热身,因为现在我们要把多线程编程和volatile联系起来了。我们通过把C 的类型世界和线程语义世界的相交之处勾画出来来做到这一点。
* 临界区之外,任何线程能够在任意时刻被任意其他线程中断,其中不存在任何控制,,所以结果是被多个线程访问的变量为volatile。这也保持了volatile的原来意图——防止编译器不小心缓存被多个线程立即用到的值。

标签:

版权申明:本站文章部分自网络,如有侵权,请联系:west999com@outlook.com
特别注意:本站所有转载文章言论不代表本站观点,本站所提供的摄影照片,插画,设计作品,如需使用,请与原作者联系,版权归原作者所有

上一篇: Command模式应用实践

下一篇: mscorwks.dll在.Net中的地位连同在.Net代码保护方面的应用