除虫记之八(WM_QUIT和结束线程:1小时/1人)
公测的时候又发现一个问题,因为一个商务上问题,程序中去掉了一个模块(dll)的调用,打包公测后,发现程序在退出后而聊天进程没有退出,等待了很长时间也没有退出。
开发人员在调试的时候,已经在debug版本下重现了这个bug,跟踪代码出错部分的流程如下:
发送WM_QUIT消息给一个工作线程让其退出;
用WaitForSingleObject等待那个线程的句柄有信号;
如果有信号,就直接CloseHandle掉这个线程句柄;
如果超时没有信号,就直接用_endthreadex()杀死线程。
开发人员跟踪了好多次发现消息发送过去后线程并没有退出,每次都要超时进入强杀流程,但强杀调用确没有杀死线程,从thread里面还可以明确的看到这个线程在运行,于是就导致了进程没有退出!
跟踪了好几次都找不到问题所在,让我过去看看。
很明显,进程没有退出,就是因为这个线程没有退出的缘故。我一开始怀疑是不是线程没有收到这个消息,于是在处理消息的地方加上断点,ft,结果频频进入断点,程序一下子就失去了反应,连VC都没有响应了,不得已在进程管理器中杀死,按说在调试状态,进程管理器是不让杀的,但不知为何偏偏杀死了,且不管他。把断点去掉,在Post线程消息前加断点,然后再在线程的消息处理中加断点。运行,Post线程消息成功,F5运行,靠,竟然没有走到线程消息处理的断点里面。线程没有收到消息????
运行多次,都是同一个现象。
因为只是一个杀死线程的操作,想让线程死还不容易啊,于是建议开发人员用TerminateThread()强杀线程,开发人员开始不理解,说线程是用_beginthreadex启动的,应该用_endthreadex杀死。我说就按我说的做,呵呵,线程如期被我们干掉了,进程退出了。
趁此机会,又给开发人员讲解了一番C运行库函数和Win32API的优劣比较。
让他们编译release版后送给测试部去验证,我又开始仔细那个线程处理消息的流程,我打了好多调试语句,怎么都发现没有收到退出的消息。晕啊。
忽然,注意到了WM_QUIT消息,哈,想起来了,自己曾经又一次就是用这个消息让线程退出,但没有成功,后来改程自定义的消息就可以了,立刻动手改成WM_USER+113,哈,收到了退出的消息,线程如期自动退出了!
哈哈,立刻又第n次的简单看了看WM_QUIT消息,然后告诉开发人员说WM_QUIT消息只能由应用程序主线程处理做程序退出。
我犯了一个大错误!
刚转了一圈回来,开发人员小声的说不是WM_QUIT消息的问题,被我听到了。
立刻追问,原来开发人员第一次的认真看了看WM_QUIT消息的MSDN文档,发现并不是线程不能处理WM_QUIT消息,而是线程中用GetMessage接收消息,而接收到这个消息后GetMessage返回0,就是这个0,被程序用if给if掉了!
我犯了一个大错误!我以为我对API已经很熟悉了,但看来远远不是!远远不是!
教训:尽可能用Win32的API,尽量的少用 c 运行库函数。WM_QUIT消息可以被任何线程处理。
--------------------next---------------------
re: 除虫记之八:WM_QUIT和结束线程
CreateThread()和_beginthreadex()在Jeffrey的《Windows核心编程》中讲的很清楚,应当尽量避免使用CreateThread()。
事实上,_beginthreadex()在内部先为线程创建一个线程特有的tiddata结构,然后调用CreateThread()。在某些非线程安全的CRT函数中会请求这个结构。如果直接使用CreateThread()的话,那些函数发现请求的tiddata为NULL,就会在现场为该线程创建该结构,此后调用EndThread()时会引起内存泄漏。_endthreadex()可以释放由CreateThread()创建的线程,实际上,在它的内部会先释放由_beginthreadex()创建的tiddata结构,然后调用EndThread()。
因此,应当使用_beginthreadex()和_endthreadex(),而避免使用CreateThread()和EndThread()。
在MFC为什么要用AfxBeginThread,因为AfxBeginThread可以更好跟MFC配合,不然微软也至于写这样的API了
你说:MFC的bug多了去了。你以为Afx函数就不会出问题?
请举例说明,否则我认为你对API一知半解造成片面认识
--------------------next---------------------
re: 除虫记之八:WM_QUIT和结束线程
为了把问题说清楚,楼主产品的代码可能大概是这个样子的
int work_thread(void *param)
{
MSG msg;
bool isExit = false;
while(!isExit)
{
if(GetMessage(&msg,0,0,0))
{
switch(msg)
{
case WM_QUIT:
isExit = true;
break;
//...
}
}
}
}
int main_thread_some_function()
{
DWORD nThreadID;
HANDLE hThread = (HANDLE)_beginthreadex( NULL, 0, &work_thread, NULL, 0, &threadID );
...
//send WM_QUIT message
PostThreadMessage(nThreadID,WM_QUIT,0,0);
int timeout = 3000;
int nResult = WaitForSingleObject(hThread,timeout);
if(nResult == WAIT_TIMEOUT) //timeout
{
_endthreadex();
}
else if(nResult == WAIT_OBJECT_0) //ok
{
CloseHandle(hThread);
}
}
这个程序的bug有两处:
一个就是if(GetMessage(&msg,0,0,0)) 处理不当
这样写
while( (bRet = GetMessage( &msg, NULL, 0, 0 )) != 0)
{
if (bRet == -1)
{
// handle the error and possibly exit
}
else
{
//process msg here
}
}
另外一个就是错误的使用了_endthreadex();这个是用来结束自己的thread,而不是来杀掉work_thread的。
所以应该改成这样
int work_thread(void *param)
{
MSG msg;
bool isExit = false;
while(!isExit)
{
int bRet = GetMessage(&msg,0,0,0);
if(bRet>0)
{
switch(msg)
{
}
}
else if(bRet == 0)
{
break;
}
else //failed
{
//process failed
}
}
}
int main_thread_some_function()
{
DWORD nThreadID;
HANDLE hThread = (HANDLE)_beginthreadex( NULL, 0, &work_thread, NULL, 0, &threadID );
...
//send WM_QUIT message
PostThreadMessage(nThreadID,WM_QUIT,0,0);
int timeout = 3000;
int nResult = WaitForSingleObject(hThread,timeout);
if(nResult == WAIT_TIMEOUT) //timeout
{
TerminateThread(nThread);
}
else if(nResult == WAIT_OBJECT_0) //ok
{
CloseHandle(hThread);
}
else
{
//error process
}
}
--------------------next---------------------
re: 除虫记之八:WM_QUIT和结束线程
原来,线程函数返回时,系统并不立即将它撤消。相反,系统要取出这个即将被撤消的线程,让它调用已经映射的DLL的所有带有DLL_THREAD_DETACH值的、且没有调用DisableThreadLibraryCalls函数的DllMain函数。DLL_THREAD_DETACH通知告诉所有的DLL执行每个线程的清除操作,例如,DLL版本的C/C++运行期库能够释放它用于管理多线程应用程序的数据块。DisableThreadLibraryCalls函数告诉系统说,特定的DLL的DllMain函数不用接收DLL_THREAD_ATTACH和DLL_THREAD_DETACH通知。
但是,系统是顺序调用DLL的DllMain函数的。当进程被创建时,系统也为该进程创建了一个互斥对象。每个进程都有它自己的互斥对象。当线程调用映射到进程的地址空间中的DLL的DllMain函数时,这个互斥对象负责对进程的所有线程实施同步。
当线程函数返回时,系统检查进程中是否存在没有调用DisableThreadLibraryCalls函数的DllMain函数,如果存在,系统就以进程的互斥对象的句柄作为第一个参数,在线程内部调用WaitForSingleObject函数。一旦这个将要终止运行的线程拥有该进程互斥对象,系统就让该线程用DLL_THREAD_DETACH的值依次调用每个没有调用DisableThreadLibraryCalls函数的DLL的DllMain函数。此后,系统才释放对进程互斥对象的所有权。
--------------------next---------------------
re: 除虫记之八:WM_QUIT和结束线程
原来,线程函数返回时,系统并不立即将它撤消。相反,系统要取出这个即将被撤消的线程,让它调用已经映射的DLL的所有带有DLL_THREAD_DETACH值的、且没有调用DisableThreadLibraryCalls函数的DllMain函数。DLL_THREAD_DETACH通知告诉所有的DLL执行每个线程的清除操作,例如,DLL版本的C/C++运行期库能够释放它用于管理多线程应用程序的数据块。DisableThreadLibraryCalls函数告诉系统说,特定的DLL的DllMain函数不用接收DLL_THREAD_ATTACH和DLL_THREAD_DETACH通知。
但是,系统是顺序调用DLL的DllMain函数的。当进程被创建时,系统也为该进程创建了一个互斥对象。每个进程都有它自己的互斥对象。当线程调用映射到进程的地址空间中的DLL的DllMain函数时,这个互斥对象负责对进程的所有线程实施同步。
当线程函数返回时,系统检查进程中是否存在没有调用DisableThreadLibraryCalls函数的DllMain函数,如果存在,系统就以进程的互斥对象的句柄作为第一个参数,在线程内部调用WaitForSingleObject函数。一旦这个将要终止运行的线程拥有该进程互斥对象,系统就让该线程用DLL_THREAD_DETACH的值依次调用每个没有调用DisableThreadLibraryCalls函数的DLL的DllMain函数。此后,系统才释放对进程互斥对象的所有权。
结果就导致了死锁。
--------------------next---------------------
re: 除虫记之八:WM_QUIT和结束线程
原来,线程函数返回时,系统并不立即将它撤消。相反,系统要取出这个即将被撤消的线程,让它调用已经映射的DLL的所有带有DLL_THREAD_DETACH值的、且没有调用DisableThreadLibraryCalls函数的DllMain函数。DLL_THREAD_DETACH通知告诉所有的DLL执行每个线程的清除操作,例如,DLL版本的C/C++运行期库能够释放它用于管理多线程应用程序的数据块。DisableThreadLibraryCalls函数告诉系统说,特定的DLL的DllMain函数不用接收DLL_THREAD_ATTACH和DLL_THREAD_DETACH通知。
但是,系统是顺序调用DLL的DllMain函数的。当进程被创建时,系统也为该进程创建了一个互斥对象。每个进程都有它自己的互斥对象。当线程调用映射到进程的地址空间中的DLL的DllMain函数时,这个互斥对象负责对进程的所有线程实施同步。
当线程函数返回时,系统检查进程中是否存在没有调用DisableThreadLibraryCalls函数的DllMain函数,如果存在,系统就以进程的互斥对象的句柄作为第一个参数,在线程内部调用WaitForSingleObject函数。一旦这个将要终止运行的线程拥有该进程互斥对象,系统就让该线程用DLL_THREAD_DETACH的值依次调用每个没有调用DisableThreadLibraryCalls函数的DLL的DllMain函数。此后,系统才释放对进程互斥对象的所有权。
结果就导致了死锁。
--------------------next---------------------