多线程错误的思考

来源:百度文库 编辑:神马文学网 时间:2024/04/29 00:21:55
多线程错误的思考
在C++,如果使用如下方式从某个方法中获得对象引用,这是一种编程错误:
Object* t = c.find(key);    //1
t->AddRef();        //2
//usage t
...
t->Release();        //3
因为在执行t->AddRef()之前,该对象有可能被别的线程从容器中删除并因引用计数回零而被销毁。此时才试图执行此句以获得引用计数已经是迟了。
在JAVA中,这样却不是一个错误,而是JAVA常用的获得引用的方法。因为JAVA对象不使用引用计数机制,它的垃圾回收器是从全局数据区和栈中遍历所有对象引用,没有引用的对象就被作为垃圾而被回收。然而我对JAVA这样的机制却不免怀疑。假如语句1执行c.find()之后,赋值给t之前,对象引用存放在CPU寄存器,但未向t写入,另一线程正好执行删除此对象的操作,并且垃圾回收器正好开始工作,垃圾回收器并不能识别存放在CPU寄存器中的引用,于是此对象的内存被回收,上面这个线程便会因操作一个不存在的对象而崩溃。出现这样的错误看起来是匪夷所思,但却是可能存在的,因为垃圾回收器仅在堆内存被耗尽时才工作,用测试手段找出这样的错误的概率几乎等于零,恐怕需要地球年龄的期望时间才能找到这样的错误。可是错误却存在,随时可能发生,一旦发生程序将立即崩溃。即使发生了,用户往往也不会很在意,还以为是别的什么外在的原因呢。可是如果在要害系统工作,这个错误真是要命的,说不定那一天宇宙飞船爆炸了,原子弹爆炸了,都是可能的。
只有二种方法能避免上述这个错误。一是赋值语句必须是原子操作,二是垃圾回收器能识别存放在CPU寄存器中的引用。JAVA能做到吗?如果赋值语句是原子操作,那么JAVA的性能会略降低,但比起系统错误来,性能下降不算什么。如果JAVA能识别存放在CPU寄存器中的引用,那么我会对JAVA的设计者们佩服的五体投地。
所以,我对用测试的方法来发现绝种应用程序的错误始终抱着怀疑的态度。在设计编写多线程程序,就应该多花费心思来精心设计和编写这样的程序而不是靠测试。目前软件界中流行的正是急功近利式的方法学。多线程编程就是极易产生错误的。著名的《tginking in java》第二版就存在这样的错误,更何况的拙劣的程序员呢。
所以发现多线程错误的最好方式是对源代码作精心的分析而不是依赖于测试。测试应该是被用于检查功能性或一般的编写错误,多线程错误却不能依赖于测试,尽管测试能发现某些这类的错误。
另一个多线程错误的常见形式如下,这个错误就见之于微软的《VC6语言参考手册》:
func() {
static bool comming = false;
if (comming) return;
comming = true;
...
comming = false;
}
上述的comming标志不能保证只有一个线程进入函数体。错误是因为读写comming变量不是原子操作,if (comming)的判断语句就更不是原子操作了。同样下面的代码也不能保证只有一个线程进入函数体:
func() {
static int comming = 0;
if (comming!=0) return;
comming++;
...
comming--;
}
因为++操作也不是原子操作。在VC++编译程序选项中增加/FAs,您会发现++操作被编译为3个指令:
mov eax,comming
inc eax
mov comming, eax
所以这也是错误的多线程编程方式,而且是常见的错误。即使在定义comming前加上关键字volatile,也不能实现原子操作。至目前为止,C++还没有提供原子操作的编译指示符或关键字。使用ASM是唯一的方法:
asm inc dword ptr comming;
if (comming!=1) return;
...
asm dec dword ptr comming;
原子操作错误的一个最著名例子是早期Intel8088CPU设置栈指针的错误:
mov ss, seg stack
mov sp, offset stack
因为栈指针由二个寄存器ss,sp组成,因此上面这个操作不是原子操作。这个错误导致系统崩溃。直到这个错误被人发现并报告之后,Intel才紧急修正了指令系统,只要向ss写操作,就自动锁定系统中断响应,直到下一个指令向sp写操作完成才解除锁定,因而在硬件级上实现这二个指令的原子操作。
另一种多线程错误的形式如下:
while(!c.empty()) {
obj = c.remove();
...
}
c.empty()测试和c.remove()操作是二个指令,这明显的不是原子操作,所以当测试!c.empty()为true之后,c中元素可能被线程销毁,obj有可能获得NULL值。正确的写法是:
while((obj=c.remove())!=NULL) {
...
}
现在,请您检查您以前写的多线程源程序,看看有没有前述这几种形式的错误,有的话赶紧改过来吧!测试是不可靠的。