曾经一个让人无语的锁

话说现在的面试经常会被问曾经遇到过哪些困难然后是怎么解决的,在这几年编程工作当中遇到的困难已经不胜枚举,费力解决的也不在少数,但是大多数问题事后再想想感觉真不值得说。但是这个曾经让我几乎是无从下手的死锁却是刻骨铭心,其实事后再想想感觉也不值得说。

其实这个问题严格来讲也不能叫做死锁,死锁的百度百科定义是:指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。类似出现可能性也很多,比如说一个线程申请内存时线程被挂起,但是申请内存锁还没释放此时其他线程都无法申请内存,但是被挂起的线程又得不到执行,于是程序就无法继续申请。还有类似低优先级线程拿到一个被锁保护的资源,但是高优先级线程恰好要等这个锁,但是低优先级线程又无法执行来释放锁于是造成优先级反转等等。而解决这类问题的方法也很多,最常见的就是用windbg直接挂上进程调试,通过加载pdb文件就直接可以定位到是哪个锁没释放,然后通过!locks命令查看当前的锁并分析,至于分析方法网上比比皆是就不多说了。

而两年前我遇到的这个问题用windbg挂上去后根本没有看到互相等待的过程,第一直觉告诉我也许是锁放在trycatch中间,然后发生异常被捕获以至于没有LeaveCriticalSection。为了增加软件的可调试性,代码压根的不能这么来,外发版本可以用异常处理避免一些崩溃,如果是内部测试怎么能这样呢?于是我直接加了一个向量化异常处理(VEH),在windows异常处理流程中,向量化异常VEH是优先于结构化异常SEH(一般的trycatch)再优于筛选器异常(一般用于生成进程dmp)。所以我把程序修改成这种模式,外发时屏蔽向量化异常。如此以来因为VEH dmp量上升了许多但是没有发现跟锁没释放相关的,也就是说排除了锁是因为异常被捕获而没能Leave的可能性。

如此以来只有慢慢分析了,由于工程比较大,!locks以后一片锁海压根的看不到一点痕迹。然后回过头来再搜代码,其实用到这个锁的线程并不多,相对而言绝对是一个很容易排除的问题,可问题是怎么就找不到原因呢?于是我干脆这个锁进去和离开的的两个线程都加上日志,但是奇怪的事情发生了,在重现这个问题的时候,日志竟然还是成双成对的输出。。。。。

也就是说程序的流程根本没问题,这么看来也符合锁上面的bug的一贯原则,真正出现问题的时候其实已经跟真实环境相差很大了,罪魁祸首已经跑到千里之外,又由于锁出现问题的时候VEH也没能捕获到异常,所以也基本排除了因为整个程序中其他地方因为异常导致的bug。

没有锁中锁,没有异常被捕获,也没有优先级反转这类高级原因,甚至连日志都成双成对的,那究竟是什么原因导致程序挂在一个锁上呢?难道是见鬼了?或者是人成鬼鬼变人呢?进去的人出来的时候就变成鬼了?

上面这句牢骚可是我当初真正发过的`(*∩_∩*)′,因为这句牢骚让我做出了一个很大的猜想:其中一个线程进锁和出锁中间,锁被改掉了!!!于是瞬间查找代码,果然发现锁有两个Init的过程!!于是赶紧修改测试,因为这个bug出现几率极低,自己编译版本连续测试好几次都不出现,也不敢保证这个问题是否真的得到解决。由于工程太大调用关系复杂,于是自己写了一个很简单的测试程序如下:

#include "stdafx.h"
#include <process.h> #include <Windows.h> CRITICAL_SECTION g_cs; UINT WINAPI LockTestThreadA(LPVOID lpParam) { EnterCriticalSection(&g_cs); InitializeCriticalSection(&g_cs); LeaveCriticalSection(&g_cs); return 0; } UINT WINAPI LockTestThreadB(LPVOID lpParam) { EnterCriticalSection(&g_cs); LeaveCriticalSection(&g_cs); return 0; } int _tmain(int argc, _TCHAR* argv[]) { HANDLE hLockTest[2]; InitializeCriticalSection(&g_cs); hLockTest[0] = (HANDLE)_beginthreadex(NULL,NULL,LockTestThreadA,NULL,NULL,NULL); Sleep(50); hLockTest[1] = (HANDLE)_beginthreadex(NULL,NULL,LockTestThreadB,NULL,NULL,NULL); WaitForMultipleObjects(2,hLockTest,TRUE,INFINITE); getchar(); return 0; }

代码写的很随意,只是个测试代码,各位看官千万别学我阿门`(*∩_∩*)′,看起来好像真的没啥问题,A进A的锁,即使被改掉了B也就进出自己的锁,各自没关系啊。但是且慢,如果A线程在Init锁的时候B恰好在等待怎么办?根据马上修改代码,瞬间使得问题重现:

#include "stdafx.h"
#include <process.h>
#include <Windows.h>

CRITICAL_SECTION    g_cs;  
UINT WINAPI LockTestThreadA(LPVOID lpParam)
{
	EnterCriticalSection(&g_cs); 
	Sleep(500);
	InitializeCriticalSection(&g_cs);
	LeaveCriticalSection(&g_cs);
	return 0;
}

UINT WINAPI LockTestThreadB(LPVOID lpParam)
{
	EnterCriticalSection(&g_cs);  
	LeaveCriticalSection(&g_cs);
	return 0;
}

int _tmain(int argc, _TCHAR* argv[])
{
	HANDLE hLockTest[2];
	InitializeCriticalSection(&g_cs);
	hLockTest[0] = (HANDLE)_beginthreadex(NULL,NULL,LockTestThreadA,NULL,NULL,NULL);
	hLockTest[1] = (HANDLE)_beginthreadex(NULL,NULL,LockTestThreadB,NULL,NULL,NULL);
    WaitForMultipleObjects(2,hLockTest,TRUE,INFINITE);
	return 0;
}

看到这里相信读者很清楚事情前因后果了,在A线程还没重新初始化锁的时候B线程已经在等待锁了,但是随后A线程将锁更改然后释放,而B线程却一直认为A并没有释放锁而始终等待着。看到这里迅速找到之前能重现的版本,重现该问题仔细看,果真发现锁的参数不一样了。。。

其实这个问题压根没什么值得可提的,在工程当中也只不过是因为一个刚加入团队的同学在一次代码提交中将一段测试代码误提交上去了,再加上这个bug在工程里面出现的概率极低所以很不好排查。但是从中我觉得还是有很多反思,比如设计模式中的单例模式,如果将锁封装起来并使用该模式便可完全避免这类问题发生。在很多人认为单例模式就是一个全局变量而不屑的时候,看到这篇文章不知道会不会有自己的感悟。同时在实际工程当中,锁最好不要使用Enter/Leave模式,可以采用类似C++智能指针模式,在构造的时候进锁析构的时候出锁,这样即使是抛异常被捕获依然能够保证锁能释放,但是前者就不行。

但是更多的时候我们要直接面对问题而不是回避问题,加trycatch可以让程序得到改正自己的机会,即使异常大部分时候依然能够执行,但是这其实是一种亚健康状态,在内测的时候更应该让程序出异常就直接告知并修正,并且加上足够的追踪信息以便将错误范围收缩到最小的范围内。

posted @ 2014-07-08 22:10  繁星jemini  阅读(312)  评论(0编辑  收藏  举报