Raymond Chen 2007年12月28日
直觉调试:诊断死锁的第一步是简单地跟踪资源
有人因为认为他们的程序界面(UI)中出现了死锁,所以向我们团队寻求帮助。(不清楚为什么他们要问我们团队,我猜是因为我们团队使用窗口管理器,他们的程序也使用窗口管理器,所以我们都在同一条船上。你可能会认为他们应该向窗口管理器团队寻求帮助。)
但事实证明,解决这个问题不需要特殊的专业知识。实际上,你可能已经知道足够的知识来解决它。
这是一些有趣的线程:
0 Id: 980.d30 Suspend: 1 Teb: 7ffdf000 Unfrozen
ChildEBP RetAddr
0023dc90 7745dd8c ntdll!KiFastSystemCallRet
0023dc94 774619e0 ntdll!ZwWaitForSingleObject+0xc
0023dcf8 774618fb ntdll!RtlpWaitOnCriticalSection+0x154
0023dd20 00cd03f2 ntdll!RtlEnterCriticalSection+0x152
0023dd38 00cd0635 myapp!LogMsg+0x15
0023dd58 00cd0c6a myapp!LogRawIndirect+0x27
0023fcb8 00cb64a7 myapp!Log+0x62
0023fce8 00cd7598 myapp!SimpleClientConfiguration::Cleanup+0x17
0023fcf8 00cd8ffe myapp!MsgProc+0x1a9
0023fd10 00cda1a9 myapp!Close+0x43
0023fd24 761636d2 myapp!WndProc+0x62
0023fd50 7616330c USER32!InternalCallWinProc+0x23
0023fdc8 76164030 USER32!UserCallWinProcCheckWow+0x14b
0023fe2c 76164088 USER32!DispatchMessageWorker+0x322
0023fe3c 00cda3ba USER32!DispatchMessageW+0xf
0023fe9c 00cd0273 myapp!GuiMain+0xe8
0023feb4 00ccdeca myapp!wWinMain+0x87
0023ff48 7735c6fc myapp!__wmainCRTStartup+0x150
0023ff54 7742e33f kernel32!BaseThreadInitThunk+0xe
0023ff94 00000000 ntdll!_RtlUserThreadStart+0x23
1 Id: 980.ce8 Suspend: 1 Teb: 7ffdd000 Unfrozen
ChildEBP RetAddr
00f8d550 76162f81 ntdll!KiFastSystemCallRet
00f8d554 76162fc4 USER32!NtUserSetWindowLong+0xc
00f8d578 76162fe5 USER32!_SetWindowLong+0x131
00f8d590 74aa5c2b USER32!SetWindowLongW+0x15
00f8d5a4 74aa5b65 comctl32_74a70000!ClearWindowStyle+0x23
00f8d5cc 74ca568f comctl32_74a70000!CCSetScrollInfo+0x103
00f8d618 76164ea2 uxtheme!ThemeSetScrollInfoProc+0x10e
00f8d660 00cdd913 USER32!SetScrollInfo+0x57
00f8d694 00cdf0a4 myapp!SetScrollRange+0x3b
00f8d6d4 00cdd777 myapp!TextOutputStringColor+0x134
00f8d93c 00cd04c4 myapp!TextLogMsgProc+0x3db
00f8d960 00cd0635 myapp!LogMsg+0xe7
00f8d980 00cd0c6a myapp!LogRawIndirect+0x27
00f8f8e0 00cd6367 myapp!Log+0x62
00f8faf0 7735c6fc myapp!remote_ext::ServerListenerThread+0x45c
00f8fafc 7742e33f kernel32!BaseThreadInitThunk+0xe
00f8fb3c 00000000 ntdll!_RtlUserThreadStart+0x23
调试死锁的关键在于,你通常不需要了解发生了什么。一旦你入门,调试过程大多是机械性的。(尽管有时很难找到最初的立足点。)
让我们看看线程0。它正在等待一个临界区。那个临界区的所有者是线程1。我怎么知道的?嗯,我可以调试它,或者我可以凭直觉说,“天哪,那个函数叫做LogMsg
,看那里有另一个线程在LogMsg
函数里面。我敢打赌那个函数使用了一个临界区来确保一次只有一个线程使用它。”
好的,所以线程0正在等待线程1。线程1在做什么?嗯,它在LogMsg
函数中进入了临界区,然后它做了一些文本处理,哦,看,它正在执行一个SetScrollInfo
。SetScrollInfo
进入了comctl32
,最终导致了一个SetWindowLong
。应用程序传递给SetScrollInfo
的窗口是由线程0拥有的。我怎么知道的?嗯,我可以调试它,或者我可以凭直觉说,“天哪,滚动信息的变化导致了窗口风格的改变,线程正试图通知窗口风格的变化。窗口显然属于另一个线程;否则我们一开始就不会陷入困境,鉴于我们只看到两个线程,没有太多选择,它可能是别的哪个线程!”
在这一点上,我认为你看到了死锁。线程0正在等待线程1退出临界区,但线程1正在等待线程0处理风格更改消息。
这里发生的情况是,程序在持有临界区的同时发送了一个消息。由于消息处理可以触发钩子和跨线程活动,你不能在发送消息时持有任何资源,因为钩子或消息接收者可能想要获取你拥有的资源,导致死锁。