关于线程完美退出的一点心得体会

日期:2014-07-28点击次数:9042

最近在测试程序时,经常发现线程无法正常退出。大致代码是这样的:

class thr_demo
{
……..
private:
HANDLE exit_thread_event;
bool m_bquit;
};
 
void thr_demo::run()
{
while(TRUE) 
{
if(WaitForSingleObject(exit_thread_event, 10) == WAIT_OBJECT_0)   
{  
break;  
}
dosomething();
}
m_bquit = true;
}
 
void thr_demo:: dosomething ()
{
//一些UI操作
}
 
void thr_demo::stop()
{
SetEvent(exit_thread_event);
while(!m_bquit)
{
Sleep(10);
}
}

这种线程退不出去的现象很随机也很频繁,首先我把目标锁定在stop()函数,既然线程无法正常退出,于是我采取了很野蛮的手段,等待500ms后,强制杀死线程。将stop()函数改成如下形式:

void thr_demo::stop()
{
if(WaitForSingleObject(thr_handle, 500) != WAIT_OBJECT_0)  
{
::TerminateThread(thr_handle, -1);
}
}
 

满以为轻松的就解决了线程无法退出的bug,但在后来的调试中发现,程序在运行过程中会无缘无故的出现异常,导致程序运行没多久就core掉。更多的异常就是引用了非法地址的数据,我马上想到了这个罪魁祸首TerminateThread,这种野蛮的杀死线程的方法,看似大快人心,目标好像是达到了,却带来了更多的隐患。
查了下TerminateThread这个函数,不使用它有N多种理由:
1、如果使用TerminateThread,那么在拥有线程的进程终止运行之前,系统不会撤销该线程的执行堆栈。原因是:如果其它正在执行的线程去引用被强制撤销的线程的堆栈上的值,那么其它的线程就会出现访问违规的问题。 
2、如果线程正在进行堆分配时被强行撤销,可能会破坏堆,造成堆锁没有释放,如果这时其他线程进行堆分配就会陷入死锁。 
3、来之MSDN的血淋淋的警告:
a) If the target thread owns a critical section, the critical section will not be released.(如果目标线程持有着一个临界区(critical section),这临界区将不会被释放。)
b) If the target thread is allocating memory from the heap, the heap lock will not be released. (如果目标线程正在堆里分配内存,堆锁(heap lock)将不会被释放。)
c) If the target thread is executing certain kernel32 calls when it is terminated, the kernel32 state for the thread's process could be inconsistent. (如果目标线程在结束时调用了某些kernel32,会造成kernel32的状态不一致。)
d) If the target thread is manipulating the global state of a shared DLL, the state of the DLL could be destroyed, affecting other users of the DLL. (如果目标线程正在更改一个共享DLL的全局状态,这个共享DLL的状态可能会被破坏,影响到其他的正在使用这个DLL库的线程。 
4、看来,TerminateThread作为终止一个线程的最后的杀招,微软也是不建议大家使用的。但是TerminateThread完全无用么?一般说来确实如此,但是如果你非常清楚你的线程在干什么,并且能够通过代码使线程在TerminateThread的时候优雅的结束,那么你可以用,但是你真的清楚么?在一个大型的工程项目中,试想,拥有10个线程、3个临界区、3个事件触发器的程序中,开发者能清楚线程在任何时刻都在干嘛么? 
5、也许结束一个线程最可靠的方法就是确定这个线程不休眠无限期的等待下去。一个支持可以被要求停止的线程,它必须定期的检查看它是否被要求停止或者如果它在休眠的话看它是否能够被唤醒。支持这两个情况最简单的的方法就是使用同步对象,这样应用程序的主线程和工作中的线程能互相沟通。当应用程序希望关闭线程时,只要设置这个同步对象,等待线程退出。之后,线程把这个同步对象作为它周期性监测的一部分,定期检查,或者如果这个同步对象的状态改变的话,就可以执行清理操作并退出了。
总之,通过上面这些就知道使用TerminateThread结束线程会给以后带来不确定的因素。要想完美的使其退出,就要使线程退出顺其自然,不野蛮不暴力,恬淡无为。于是我彻底的放弃了这种暴力杀线程的方法,将stop()函数改成了初始的状态。
我把目标转向了m_bquit这个成员变量,首先我怀疑这个变量在我退出时值的正确性。因为它作为线程退出标识,它的值关系到stop函数会不会一直在sleep等待。查了下资料,我认为m_bquit这个变量应该是被编译器优化过的,编译器可能优化读取和存储,可能暂时使用寄存器中的值,如果这个变量由别的程序更新了的话,将出现不一致的现象。我马上想到了一个阻止编译器优化某变量的关键字volatile,加上这个关键字后,volatile提醒编译器它后面所定义的变量随时都有可能改变,因此编译后的程序每次需要存储或读取这个变量的时候,都会直接从变量地址中读取数据。
于是我将类定义改为:

class thr_demo
{
……..
private:
HANDLE exit_thread_event;
volatile bool m_bquit;
};
 

再次将程序跑起来,运行了一个多小时,程序没出现界面僵死状态,因为界面僵死,我就知道一定是线程没退出导致的。好景不长,我突然发现我的界面不再切换了,把断点打在stop()函数中的sleep行,发现线程无法退出,正在死循环状态。我百思不得其解,除非是我逻辑操作中有问题,导致dosomething()这个函数无法正常退出。于是我在这个函数上一行和下一行都打印了一条日志,如果是这个函数无法正常退出的话,打印的日志就是不对称的。在漫长的等待后,这种现象复现了。我猜测的没错,dosomething()这个函数确实是没正常退出来,好像是死锁了。
其实dosomething()这个函数也是蛮简单的,代码大致如下:

void thr_demo:: dosomething ()
{

//一些UI操作
switch(op)
{
case LINK_FAIL:

m_wnd.Invalidate();
break;
case LINK_OK:

m_wnd.Invalidate();
break;
default:
break;
}
}
 

 
看到这么简单的代码,怎么可能会发生死锁,导致函数无法正常退出?无法相信自己的眼睛,但是再运行几次,都是发现这里面有问题。由于函数简单,不可能有死循环这种现象发生,于是我把眼光放在了这些UI操作函数上,如m_wnd.Invalidate();这些界面刷新函数。莫非这些UI操作放在这里是不适宜的?
我马上开始联想了,我通过菜单点击停止线程时,stop()函数此时是处于窗口的消息处理函数中的,由于此时碰巧调用Invalidate(),Invalidate()其实是要调用SendMessage函数,SendMessage是同步操作,要完成操作才能返回(也要在窗口的消息处理函数中处理),我认为是SendMessage函数调用阻塞了stop()函数的退出。
而且查资料发现,MFC的控件不是线程安全的,所以在线程中操作界面是一件很危险的事情。所以就需要安全的方法。
于是想到了自定义消息,打算把UI操作放在UI主线程中处理,首先将代码改成这样:

void thr_demo:: dosomething ()
{

//一些UI操作
switch(op)
{
case LINK_FAIL:

myWnd.SendMessageA(WM_UPDATEUIMSG, 0,op);
break;
case LINK_OK:

myWnd.SendMessageA(WM_UPDATEUIMSG, 0,op);
break;
default:
break;
}
}
 

试着运行程序,界面还是会死锁,让我想到,SendMessage操作不能放在这里面,查了查SendMessage和PostMessage的区别:
PostMessage执行后马上返回。
PostMessage 将消息放入消息队列中,自己立刻返回,消息循环中的 GetMessage(PeekMessage)处理到我们发的消息之后,便按照普通消息处理方法进行处理。
SendMessage必须等到消息被处理后才会返回。
SendMessage 内部调用 SendMessageW、SendMessageWorker 函数做内部处理,然后调用 UserCallWinProcCheckWow、InternalCallWinProc 来调用我们代码中的消息处理函数,消息处理函数完成之后,SendMessage 函数便返回了。
于是我把SendMessage换成PostMessage,将界面操作交给窗口Message Loop处理,问题才得以解决。
关于线程如何完美退出的问题,其实不简单。上面遇到的情况其实还是简单的。当你在线程中使用了互斥锁,递归锁时,问题更复杂,一不小心,你就可能造成程序死锁。
像这种锁法是不推荐的:

CSingleLock sl(&mutex1); 
sl.Lock();

CSingleLock s2(&mutex2);
s2.Lock();

s2.UnLock();

s1.UnLock();

这种代码,除非是万不得已,否则不应该去嵌套,这种情形很容易造成死锁。如果有可能,要改成这种情形:

CSingleLock sl(&mutex1); 
sl.Lock();


s1.UnLock();

CSingleLock s2(&mutex2);
s2.Lock();

s2.UnLock();

另外也不推荐递归锁(可重入锁),这种锁,不是互斥锁,能调用Lock();多次,但是解锁UnLock()操作要和Lock()次数相同。  所谓递归锁,就是在同一线程上该锁是可重入的,对于不同线程则相当于普通的互斥锁。在同一线程上是不会形成死锁的,但此时如果其他线程想要加锁,只有等待拥有锁的线程释放所有的锁。(加锁几次要释放几次)
总之,关于线程和锁产生的问题,都是不那么容易解决,需要自己在实战中摸索和推敲,实践出真知嘛。


 

软件部   向国春