2017-03-06

作死的TerminateThread

有一天早上,我收到了一个来自同事的Dump文件。打开一调试,报错信息如下:
0x77B16BB9 (ntdll.dll) (XXX.dmp 中)处有未经处理的异常:  0xC0000005:  读取位置 0x1C7695E8 时发生访问冲突。
我一般首先会去看CallStack,因为PDB都有,所以得到的情况很清晰:
ntdll.dll!RtlpWakeByAddress()
ntdll.dll!RtlpUnWaitCriticalSection()
ntdll.dll!_RtlLeaveCriticalSection@4()
XXX.dll!RecMutex::unlock()
XXX.dll!LockT<RecMutex>::~LockT<RecMutex>()
……
这个调用栈看得我有点糊涂。这个RecMutex来自ICE的最新版本,我从1.3.0版就开始用ICE,用到现在的3.6.3。这种基础的代码我有信心不会有问题,也不会用出问题来。何况RecMutex只是对CRITICAL_SECTION的一个简单封装,代码并不多,也并不复杂。我这次在自己项目代码中用之前恰好看过一遍,也认为这里面不会有什么问题。
具体到RecMutex::unlock(),其实也就是干了这个事情:
void RecMutex::unlock() const
{
if (--_count == 0)
{
LeaveCriticalSection(&_mutex);
}
}
这个问题并不是每次都能重现。于是可以肯定跟“多线程”有关系。很显然,LeaveCriticalSection()这种级别的Win32 API调用绝对不会存在着会导致Crash的Bug,否则微软早就死翘翘了,所以问题肯定出在那个_mutex上。然而,既然if语句能进来,那么this指针一般来说是对的。因为是Release版本,优化导致了LocalVar的显示不是那么可靠,不过跟踪ESP的情况最终验证了我的猜测是对的——至少到这个时候,栈还没有坏掉。

这个_mutex就是一个CRITICAL_SECTION。那么我就很头大了。多线程出问题,一般是临界区被搞坏了。然而CRITICAL_SECTION本身就是用来保护临界区的。它什么情况下会被搞坏掉呢?

接下来的汇编代码,Debug起来就很有些难度了。我只能大致确认,在_RtlLeaveCriticalSection()中,_mutex还没有坏掉。但在RtlpWakeByAddress()中就肯定坏了。至于RtlpUnWaitCriticalSection()的情况就不是很清楚了。

到此时,三个小时已经过去了。正面Debug似乎走入了泥沼,现在只剩下“程序不会闹鬼,一定要查个水落石出”的精神还在支撑着我。起来上了个厕所,我换了条思路:问Google。
Google果然厉害,哪怕只是简单的关键词搜索,第一条搜素结果就直指要害

其实第二条搜素结果也很正确,不过Reddit毕竟还是没有StackOverflow专业。这个问题所描述的现象,跟我遇到的是一模一样。而且给我Dump文件的那个同事用的OS的确是Win10
。我最感激的是这哥们问题的开头一段几乎直接就回答了我的疑惑:TerminateThread。该死的!肯定有人在做这种事情。

我当然是知道的。TerminateThread会造成各种各样千奇百怪的问题。正常的应用层操作的确不太可能造成CRITICAL_SECTION本身被搞坏(除了把栈写坏),然而TerminateThread这种东西是一定会干得出来的。大多数程序员都知道TerminateThread不靠谱,所以都不会去用,这个已经几乎成为常识了。于是有一个坑爹的后果就是:一旦有人真的这样干了,正常人全部懵圈,因为没人想到会是这个原因。多谢这哥们把这种问题给问了出来,否则我可能还要花更多的时间在它上面。

接下来的事情就很简单,找出谁在用TerminateThread。其实也没那么简单,因为我找了一圈,都没发现相关的代码。我都有点开始怀疑自己是不是碰到了别的情况。这个时候我突然想到:会不会是别人的库里面有问题呢?

我当然用到了一些开源第三方库,不过我相信它们不会犯这种错误。它们就算要用到TerminateThread,也肯定是作为程序退出之前的强制性措施来使用,正常运行情况下是绝对不会这样用的。

相比较而言,我比较不信任其它组同事写出来的“中间件”。一问之下,那个写“中间件”的同事,也用了另外一个组提供给他的一些开发库。再追着问,果然,那个开发库里面对线程的封装,在退出“超时”的情况下调用了TerminateThread。

至此,水落石出了。这个Dump文件的确就是在调用了那个中间件的关闭接口之后不远的地方产生的,并且产生之前确实很明显地出现了“超时”的现象——界面卡住了约5秒没有动。可想而知,由于停线程超时,于是就去调用了TerminateThread。那个开发库的编写者大概以为,这样就可以让程序继续跑下去。然而这只是他的一厢情愿。用过TerminateThread之后,系统的行为就会是undefined的了。现在可能不会Hang,但是接下来会出现各种莫名其妙的错误,到那个时候必然将会更难调试。这种“保证线程可以被停掉”的代码我宁可不要。浪费时间,不就是在浪费生命么?

在最后,再次警告大家:珍爱生命,远离TerminateThread。

2 条评论:

  1. 博主,这个问题最后咋解决了

    回复删除
    回复
    1. 叫那个写开发库的同事把代码改掉别用TerminateThread。

      删除