暂时不用标题

暂时无子标题
随笔 - 25, 文章 - 1, 评论 - 181, 引用 - 0

导航

<2008年8月>
272829303112
3456789
10111213141516
17181920212223
24252627282930
31123456

留言簿(3)

随笔档案

文章档案

相册

搜索

最新评论

阅读排行榜

评论排行榜

除虫记之八:WM_QUIT和结束线程

除虫记之八(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消息可以被任何线程处理。

posted on 2006-01-06 10:02 糖水煎包 阅读(5207) 评论(25)  编辑 收藏

评论

# re: 除虫记之八:WM_QUIT和结束线程

不明白为什么大家都喜欢用
WaitForSingleObject
来等待线程退出。
2006-01-06 10:29 | pAnic

# re: 除虫记之八:WM_QUIT和结束线程

我现在对你们产品基本上没什么信心了

TerminateThread和_endthreadex的区别你们都不了解,瞎用
TerminateThread是别的thread来杀掉某个thread,由于不能正确释放资源,不建议使用
_endthreadex是thread自己来退出的API

你说: 尽可能用Win32的API,尽量的少用 c 运行库函数
这句话是不对的,请问创建线程最好用_beginthreadex还是CreateThread?

如果是MFC工程,应该用AfxBeginThread
2006-01-06 10:29 | 小明

# to:小明

小明呀,告诉你,记住了,在Win32平台上,不管是_beginthreadex 还是AfxBeginThread最终的实现都是用CreateThread来创建线程的。
不知道的不要乱说。

MFC的bug多了去了。你以为Afx函数就不会出问题?

使用c运行库,很多地方要调用者进行陷阱检查和异常处理。windows的API是做了基本容错的,对一个项目来讲,要求的是稳定运行,用什么方式实现都无所谓的,很明显,在健壮性上,win32API要比c运行库函数健壮多了。
这点也要一定记住了。不要到处说应该这样、应该那样的。




2006-01-06 13:41 | hyj

# 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一知半解造成片面认识


2006-01-06 14:04 | 小明

# to :小明

to:小明
1。第一段话不是自己的原创,要注明出处!

2。“在某些非线程安全的CRT函数中会请求这个结构。”这句话的含义你理解了吗?

3。MFC的bug,网上很多的,你搜索一下,然后验证就知道了。

2006-01-06 14:25 | hyj

# re: 除虫记之八:WM_QUIT和结束线程

呵呵,路过。描述的很有意思。喜欢此系列
2006-01-06 14:45 | flyingleaf

# re: 除虫记之八:WM_QUIT和结束线程

另外声明;不参与江湖殴斗 :P

btw:我就喜欢声明。
2006-01-06 14:45 | flyingleaf

# re: 除虫记之八:WM_QUIT和结束线程

呀,被你发现不是原创的,你眼光不错嘛。可是这个是重点么?

1.使用_beginthreadex而不是CreateThread来创建thread
2.MFC中使用AfxBeginThread来创建thread,AfxBeginThread也是调用
_beginthreadex来创建thread
3.不到万不得已,不使用TerminateThread来杀掉thread
4.尽可能使用C API,为将来可能的移植做准备
5.调用Win32 API,必须要检查返回值。

楼主你心胸开阔一点嘛
你能花时间把你遇到的bug都写出来,我还是支持的
2006-01-06 14:46 | 小明

# re: 除虫记之八:WM_QUIT和结束线程

to 小明:
楼主都说了,杀那个线程后就结束进程了,还执拗于TerminateThread和_endthreadex干什么呢?

win32api、crt、mfc几个开关线程的技术并不是什么难点,CreateThread等也不是什么万万碰不得的东西,这你也提到了

知识不了解是可以学习的,对于有意识的人类来说,灵活运用知识更为关键,拘泥于条条杠杠要不得
2006-01-06 14:54 | ork

# to : ork

你说:
知识不了解是可以学习的,对于有意识的人类来说,灵活运用知识更为
关键,拘泥于条条杠杠要不得

我赞同
但是更多的人学习不求甚解,一知半解,写出来的程序,哎。

另外TerminateThread和_endthreadex差别太大,一个是杀别人,一个是自杀,绝对不能混淆
2006-01-06 14:59 | 小明

# re: 除虫记之八:WM_QUIT和结束线程

to 小明:
你都赞同“不要拘泥于。。。”了还说“。。。差别太大。。。”?不是你太教条就是明摆着挑刺

要是此例中工作线程能收到结束消息他当然会自己_endthreadex,问题是bug未排除时他没收到,那咋办?权宜之计,只能terminate之了啊,然后我说,既然结束线程后紧接着结束进程,两者差别有多少呢?你说是不是?
2006-01-06 15:05 | ork

# 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
}
}

2006-01-06 16:27 | 小明

# re: 除虫记之八:WM_QUIT和结束线程

我前一段碰到一个问题,在线程退出时,发现在执行_endthreadex的时候挂住了,不知道会是什么原因。
2006-01-18 14:03 | fslife

# 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函数。此后,系统才释放对进程互斥对象的所有权。
2006-03-14 11:10 | 顾茂强

# 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函数。此后,系统才释放对进程互斥对象的所有权。 
结果就导致了死锁。
2006-03-14 11:12 | 顾茂强

# 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函数。此后,系统才释放对进程互斥对象的所有权。  
结果就导致了死锁。 
2006-03-14 11:12 | 顾茂强

# re: 除虫记之八:WM_QUIT和结束线程

  很显然的一个教训就是在DllMain内部应该避免任何Wait*调用。但是进程互斥对象的问题不仅仅限于Wait*函数。操作系统在CreateProcess、GetModuleFileName、GetProcAddress、LoadLibrary和FreeLibrary等函数中在后台获取进程互斥对象,因此在DllMain中不应该调用任何这些函数。因为DllMain获取进程互斥对象,所以一次只能有一个线程执行DllMain。

-----------
还是好好学学操作系统再查这种问题
2006-03-14 11:15 | 顾茂强

# re: 除虫记之八:WM_QUIT和结束线程

以上是我写的文档中摘录的话,是搂主所述bug的根本原因。从我的经验来看,搂主不懂这样的原因也挺正常,因为很多人不懂,都以为线程函数退出后线程就应该也终止了,实际上不是这样
2006-03-16 08:23 | 顾茂强

# to 顾茂强

你也太有意思了,《windows核心编程》也不是什么神你的读物,你读过就读过,还“还是好好学学操作系统再查这种问题”、“从我的经验来看,搂主不懂这样的原因也挺正常,因为很多人不懂”一堆话,真是好笑

再说了,是你说的这个原因么?再给你一次机会,你再想想?呵呵
2006-03-16 11:44 | kwy

# re: 除虫记之八:WM_QUIT和结束线程

两小儿辨日
2006-03-21 16:46 | niyeye

# re: 除虫记之八:WM_QUIT和结束线程

《windows核心编程》中只讲了winmain中创建线程并等待之时,线程函数一直得不到运行,没见过其中还讲过线程结束不了的具体原因。但是从中推测一下可以知道这个结论。。

另外,讲技术问题 没有必要人身攻击吧。楼上的niyeye ,以为如何
2006-03-22 09:03 | 呵呵

# re: 除虫记之八:WM_QUIT和结束线程

作了一个测试:
程序中只有一个DLL,模仿楼主的程序,不管这个DllMain是否调用DisableThreadLibraryCalls,程序都能正常退出;

但是,如果有两个以上DLL,且有DLL的DllMain没有调用DisableThreadLibraryCalls话,程序就不能正常结束线程。



顾茂强所述的原因是有道理的。
2006-03-22 11:45 | xyz

# re: 除虫记之八:WM_QUIT和结束线程

谢谢你的文章
2007-02-01 16:50 | 网络电话

# TerminateThread杀不了线程怎么回事?

BOOL CABDOTAmfcDlg::OnInitDialog()
{

...
//开始一个tcp线程
extern CWinThread *DotaThread;
extern UINT DotaTcpThread(LPVOID pParam);
DotaThread=AfxBeginThread(DotaTcpThread,0,0,0,NULL);
return TRUE;  // 除非将焦点设置到控件,否则返回 TRUE
}
void CABDOTAmfcDlg::OnDestroy()
{

extern CWinThread *DotaThread;
TerminateThread(DotaThread->m_hThread,0);
}
UINT DotaTcpThread(LPVOID pParam)
{
while(1)
{
DotaTcp.ReadPacket();
}
return 0;
}
对话框初始化和结束,调试的时候都进入了这2个函数,TerminateThread返回true,但是任务管理器中还存在该进程。怎么回事?是不是调用有问题什么的?

期望解答,或者给个提示~~
谢了~
2008-08-19 21:38 | badming

# re: 除虫记之八:WM_QUIT和结束线程

get wm_quit消息返回0属于很重要的细节

2本书,window程序设计 window核心编程

某些人看来这两本书都没看仔细,一知半解揣测问题所在
2008-09-16 17:15 | 说2句
标题  
姓名  
主页
验证码 *
内容   
  登录  使用高级评论  Top
[使用Ctrl+Enter键可以直接提交]