一个跨线程创建窗口的死锁案例
转载自: http://blog.titilima.com/createwindow-deadlock.html
出于某种需要,我们有时可能会实现一个如下描述的场景:
- 在线程 A 中,创建一个窗口,称为窗口 X。
- 窗口 X 创建后,创建一个子窗口,称为窗口 Y,并且 Y 所属一个新线程,称为线程 B。
简单来说,父子窗口分别所属不同的线程。
需求描述完毕,现在进入实现的阶段。我以一个简单的例子来实现这个场景,其中 X 为一个自定义窗口,Y 为一个按钮。为了使 按钮从属线程 B,那么我们需要在线程 B 中创建它,并实现其消息队列的分发。另外,父窗口在某个时机(比如 WM_CREATE)创建线程 B。最后,父窗口希望能够监视到子窗口创建成功,因此用了一个事件(Event)来实现线程的同步,大致代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
// 线程参数结构 typedef struct _tagCreateParam { HWND hParent; HANDLE hEvent; HWND hBtn; } CREATEPARAM, *PCREATEPARAM; // 按钮线程 DWORD WINAPI BtnThread(PVOID param) { PCREATEPARAM p = (PCREATEPARAM)param; p->hBtn = CreateWindow(WC_BUTTON, _T("Button"), WS_CHILD | WS_VISIBLE, 10, 10, 100, 50, p->hParent, (HMENU)1000, g_hInst, NULL); SetEvent(p->hEvent); MSG msg; while (GetMessage(&msg, NULL, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); } return 0; } // 父窗口的 WM_CREATE 处理器 BOOL OnCreate(HWND hwnd, LPCREATESTRUCT lpCreateStruct) { CREATEPARAM cp = { 0 }; cp.hParent = hwnd; cp.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL); g_hBtnThread = CreateThread(NULL, 0, BtnThread, &cp, 0, NULL); WaitForSingleObject(cp.hEvent, INFINITE); return TRUE; } |
代码看起来很合理。不过当它实际跑起来的时候,你会发现这个进程发生了死锁,没有任何反应。
好,下面我的分析开始。如果你知道死锁的原因,那么就可以飘过了。——当然,如果你只知其然不知其所以然,下面的文字应该还是有些用处的。
首先,我们在调试器中将死锁进程暂停。我们知道,线程 A 肯定是死在了 WaitForSingleObject 上,所以无视之,直接查看 BtnThread 的堆栈,如下图。
很可惜,这里没什么有用的信息。于是我们不得不进到内核之中,启动 LiveKd,找到我们的死锁进程。
PROCESS 8846b3a0 SessionId: 0 Cid: 19a0 Peb: 7ffd4000 ParentCid: 0a68 DirBase: 0ac802e0 ObjectTable: e729e9f0 HandleCount: 48. Image: CreateWindowDeadLock.exe |
接下来查看其详细信息,文本很多,不要被弄晕了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 |
0: kd> !process 8846b3a0 PROCESS 8846b3a0 SessionId: 0 Cid: 19a0 Peb: 7ffd4000 ParentCid: 0a68 DirBase: 0ac802e0 ObjectTable: e729e9f0 HandleCount: 48. Image: CreateWindowDeadLock.exe VadRoot 883463d0 Vads 53 Clone 0 Private 153. Modified 5. Locked 0. DeviceMap e2ece800 Token e51e7d20 ElapsedTime 00:07:34.421 UserTime 00:00:00.046 KernelTime 00:00:00.828 QuotaPoolUsage[PagedPool] 51068 QuotaPoolUsage[NonPagedPool] 2120 Working Set Sizes (now,min,max) (745, 50, 345) (2980KB, 200KB, 1380KB) PeakWorkingSetSize 749 VirtualSize 19 Mb PeakVirtualSize 23 Mb PageFaultCount 769 MemoryPriority BACKGROUND BasePriority 8 CommitCharge 247 DebugPort 87e47780 THREAD 8a3d8020 Cid 19a0.1e68 Teb: 7ffdf000 Win32Thread: e4934a30 WAIT: (UserRequest) UserMode Non-Alertable 87fda2b8 NotificationEvent Not impersonating DeviceMap e2ece800 Owning Process 0 Image: Attached Process 8846b3a0 Image: CreateWindowDeadLock.exe Wait Start TickCount 2036366 Ticks: 26911 (0:00:07:00.484) Context Switch Count 305 LargeStack UserTime 00:00:00.031 KernelTime 00:00:00.000 Win32 Start Address 0x004111ef Start Address kernel32!BaseProcessStartThunk (0x7c810705) Stack Init a5d0c740 Current a5d0c3e0 Base a5d0d000 Limit a5d09000 Call a5d0c74c Priority 9 BasePriority 8 PriorityDecrement 0 DecrementCount 16 Kernel stack not resident. ChildEBP RetAddr a5d0c3f8 80504850 nt!KiSwapContext+0x2f (FPO: [Uses EBP] [0,0,4]) a5d0c404 804fc078 nt!KiSwapThread+0x8a (FPO: [0,0,0]) a5d0c42c 805c176c nt!KeWaitForSingleObject+0x1c2 (FPO: [5,5,4]) a5d0c490 8054263c nt!NtWaitForSingleObject+0x9a (FPO: [Non-Fpo]) a5d0c490 7c92e514 nt!KiFastCallEntry+0xfc (FPO: [0,0] TrapFrame @ a5d0c4a4) 0012f480 00000000 ntdll!KiFastSystemCallRet (FPO: [0,0,0]) THREAD 87fac560 Cid 19a0.1844 Teb: 7ffde000 Win32Thread: e726aeb0 WAIT: (WrUserRequest) UserMode Non-Alertable 884d66b0 SynchronizationEvent Not impersonating DeviceMap e2ece800 Owning Process 0 Image: Attached Process 8846b3a0 Image: CreateWindowDeadLock.exe Wait Start TickCount 2038243 Ticks: 25034 (0:00:06:31.156) Context Switch Count 41 NoStackSwap LargeStack UserTime 00:00:00.000 KernelTime 00:00:00.000 Win32 Start Address 0x00411226 Start Address kernel32!BaseThreadStartThunk (0x7c8106f9) Stack Init a5a72000 Current a5a719f0 Base a5a72000 Limit a5a6e000 Call 0 Priority 10 BasePriority 8 PriorityDecrement 0 DecrementCount 16 ChildEBP RetAddr a5a71a08 80504850 nt!KiSwapContext+0x2f (FPO: [Uses EBP] [0,0,4]) a5a71a14 804fc078 nt!KiSwapThread+0x8a (FPO: [0,0,0]) a5a71a3c bf802f45 nt!KeWaitForSingleObject+0x1c2 (FPO: [5,5,4]) a5a71a78 bf840f3c win32k!xxxSleepThread+0x192 (FPO: [3,5,4]) a5a71b14 bf8141ba win32k!xxxInterSendMsgEx+0x7f6 (FPO: [Non-Fpo]) a5a71b60 bf80ecc1 win32k!xxxSendMessageTimeout+0x11f (FPO: [7,7,0]) a5a71b84 bf83e1d0 win32k!xxxSendMessage+0x1b (FPO: [4,0,0]) ; <-- 注意这里 a5a71c6c bf834af7 win32k!xxxCreateWindowEx+0xd0d (FPO: [15,49,0]) a5a71d20 8054263c win32k!NtUserCreateWindowEx+0x1c1 (FPO: [Non-Fpo]) a5a71d20 7c92e514 nt!KiFastCallEntry+0xfc (FPO: [0,0] TrapFrame @ a5a71d64) 00abfd98 00000000 ntdll!KiFastSystemCallRet (FPO: [0,0,0]) |
请注意第 67 行。如无意外,我们的死锁应该是发生在 SendMessage 里面了。查看其调用参数,如下。
0: kd> dd a5a71b84 l6 a5a71b84 a5a71c6c bf83e1d0 bbf44570 00000210 ; <-- 注意这里 a5a71b94 03e80001 00101100 |
好了,答案已经浮出水面。0×210 这个数值对应的消息是 WM_PARENTNOTIFY。当子窗口创建时,会向其父窗口(窗口 X)发送这个消息并无限等待。但是,窗口 X 所在的线程 A 正在 WaitForSingleObject,无法进行消息的处理,因此造成了两个线程的互锁。
文中提到的测试代码见附件。
附件:createwindowdeadlock.zip