死锁场景描述:
针对之前一个版本反馈回来的问题,对数据通讯模块升级,做了精简和重构
因为ABA问题的存在,将之前以Socket为key改为以只增的Int为key。使用的锁为临界区锁。
修改完成,联调后进行压力测试,发现当后台的线程池满的时候会必然发生死锁。
死锁定位过程:
第一步:精简线程模型,将授权检测线程、超时检测等辅助线程通通屏蔽。
大压力测试,仍然死锁。
第二步: 在第一步的基础上,将通讯工作线程减到1个。
大压力测试,仍然死锁。
因为涉及模块比较多,为了快速定位问题,使用windbg来检测死锁点
第三步:使用windbg
当应用程序挂死后,使用windbg Attach到进程。
1. 使用 ~*kb,查看堆栈信息
- 8 Id: 368.bd4 Suspend: 1 Teb: 7ffd6000 Unfrozen ChildEBP RetAddr Args to Child 0614fd4c 7c92df5a 7c939b23 00000604 00000000 ntdll!KiFastSystemCallRet 0614fd50 7c939b23 00000604 00000000 00000000 ntdll!NtWaitForSingleObject+0xc 0614fdd8 7c921046 0012fe08 5f401b5f 0012fe08 ntdll!RtlpWaitForCriticalSection+0x132 0614fde0 5f401b5f 0012fe08 0012fdfc 0614fedc ntdll!RtlEnterCriticalSection+0x46 0614fdf0 0040b207 ffffffff 77d2b326 03103ee0 MFC42D!CCriticalSection::Lock+0x14 0614fedc 5f438514 0012fd30 ffffffff 77d2b326 RecExtraction!CRecExtractionModule::ThreadAnalysis+0x185 [G:\Code\vc2012\VSIP\DigitalViewer_V1\RecExtraction\RecExtractionModule.cpp @ 331] 0614ff80 1020c323 0012f4f4 ffffffff 77d2b326 MFC42D!_AfxThreadEntry+0x2c4 0614ffb4 7c80b729 03103ee0 ffffffff 77d2b326 MSVCRTD!_beginthreadex+0x133 0614ffec 00000000 1020c2b0 03103ee0 00000000 kernel32!BaseThreadStart+0x37
其中,上图中的8是线程编号,368是进程ID,bd4是线程ID。
可以看到ntdll!NtWaitForSingleObject ,没有拿到锁,说明死锁发生了。
- 0614fd4c 7c92df5a 7c939b23 00000604 00000000 ntdll!KiFastSystemCallRet
- 0614fd50 7c939b23 00000604 00000000 00000000 ntdll!NtWaitForSingleObject+0xc
- 0614fdd8 7c921046 0012fe08 5f401b5f 0012fe08 ntdll!RtlpWaitForCriticalSection+0x132
- 这里的第一个是ebp,第二个是返回函数地址,接下来才是函数的参数。
要看下比如 ntdll!NtWaitForSingleObject参数定义:
- NTSTATUS WINAPI NtWaitForSingleObject(
- _In_ HANDLE Handle,
- _In_ BOOLEAN Alertable,
- _In_ PLARGE_INTEGER Timeout
- );
所以 00000604 00000000 00000000分别对应上面的Handle,Alrtable,Timeout.
如上所述,handle是00000604,
- !cs 00000604
再看一个网上的例子:
0:004> ~1kb
ChildEBP RetAddr Args to Child
0051fe48 7c92df5a 7c939b23 00000034 00000000 ntdll!KiFastSystemCallRet
0051fe4c 7c939b23 00000034 00000000 00000000 ntdll!NtWaitForSingleObject+0xc
0051fed4 7c921046 00417140 00411420 00417140 ntdll!RtlpWaitForCriticalSection+0x132
*** WARNING: Unable to verify checksum for D:\Project1\test2\Debug\test2.exe
0051fedc 00411420 00417140 00000000 00000000 ntdll!RtlEnterCriticalSection+0x46
0051ffb4 7c80b729 00000000 00000000 00000000 test2!thread1+0x50 [d:\project1\test2\test2\test2.cpp @ 10]
0051ffec 00000000 00411122 00000000 00000000 kernel32!BaseThreadStart+0x37
0:004> !cs 00417140
-----------------------------------------
Critical section = 0x00417140 (test2!cs2+0x0)
DebugInfo = 0x7c99ea00
LOCKED
LockCount = 0x2
OwningThread = 0x00001f60
RecursionCount = 0x1
LockSemaphore = 0x34
SpinCount = 0x00000000
可以看出该临界区的持有者(OwningThread)是 0x00001f60
这个地址是线程在内核中的ID,需要转换为用户态的ID,使用~~[]命令。
- ~~[线程内核id]
0:003> ~~[0x0000185c]
2 Id: 1a98.185c Suspend: 1 Teb: 7ffdd000 Unfrozen
Start: test2+0x1030 (00401030)
Priority class: 32; Affinity: f
通过转换,可以看到锁的持有者是2号线程。
这样就可以很明确的找到锁的持有者了,如果pdb加载正确,通过~2kb就已经可以对应到代码行了。
查找死锁很困难,但是一旦找到点,解决起来就容易多了。
以上的例子部分来自网贴,在此对原作者表示感谢。
---------------------------------
对于死锁的小结:
1. 对上层提供的函数,需要通过设计,规避操作带锁。
2.在锁中避免调用回调,因为回调的操作不可预知,易发生死锁。