如果您将 MessageBox.Show 调用替换为 Debugger.Break 并在中断命中时附加一个启用本机调试的调试器,您可以看到发生了什么。调用堆栈如下所示:
WindowsFormsApplication3.exe!WindowsFormsApplication3.Form1.notifyIcon1_MouseClick(object sender = {System.Windows.Forms.NotifyIcon}, System.Windows.Forms.MouseEventArgs e = {X = 0x00000000 Y = 0x00000000 Button = Left}) Line 30 + 0x1e bytes C#
System.Windows.Forms.dll!System.Windows.Forms.NotifyIcon.OnMouseClick(System.Windows.Forms.MouseEventArgs mea) + 0x6d bytes
System.Windows.Forms.dll!System.Windows.Forms.NotifyIcon.WmMouseUp(ref System.Windows.Forms.Message m, System.Windows.Forms.MouseButtons button) + 0x7e bytes
System.Windows.Forms.dll!System.Windows.Forms.NotifyIcon.WndProc(ref System.Windows.Forms.Message msg) + 0xb3 bytes
System.Windows.Forms.dll!System.Windows.Forms.NotifyIcon.NotifyIconNativeWindow.WndProc(ref System.Windows.Forms.Message m) + 0xc bytes
System.Windows.Forms.dll!System.Windows.Forms.NativeWindow.Callback(System.IntPtr hWnd, int msg = 0x00000800, System.IntPtr wparam, System.IntPtr lparam) + 0x5a bytes
user32.dll!_InternalCallWinProc@20() + 0x23 bytes
user32.dll!_UserCallWinProcCheckWow@32() + 0xb3 bytes
user32.dll!_DispatchClientMessage@20() + 0x4b bytes
user32.dll!___fnDWORD@4() + 0x24 bytes
ntdll.dll!_KiUserCallbackDispatcher@12() + 0x2e bytes
user32.dll!_NtUserPeekMessage@20() + 0xc bytes
user32.dll!__PeekMessage@24() + 0x2d bytes
user32.dll!_PeekMessageW@20() + 0xf4 bytes
ole32.dll!CCliModalLoop::MyPeekMessage() + 0x30 bytes
ole32.dll!CCliModalLoop::PeekRPCAndDDEMessage() + 0x30 bytes
ole32.dll!CCliModalLoop::FindMessage() + 0x30 bytes
ole32.dll!CCliModalLoop::HandleWakeForMsg() + 0x41 bytes
ole32.dll!CCliModalLoop::BlockFn() - 0x5df7 bytes
ole32.dll!_CoWaitForMultipleHandles@20() - 0x51b9 bytes
WindowsFormsApplication3.exe!WindowsFormsApplication3.Form1.notifyIcon1_MouseClick(object sender = {System.Windows.Forms.NotifyIcon}, System.Windows.Forms.MouseEventArgs e = {X = 0x00000000 Y = 0x00000000 Button = Left}) Line 32 + 0x14 bytes C#
相关函数是 CoWaitForMultipleHandles。它确保 STA 线程在没有仍然发送消息的情况下不能阻塞同步对象。这是非常不健康的,因为它很可能导致死锁。特别是在 NotifyIcon 的情况下,因为阻止通知消息会挂起托盘窗口,使所有图标都不起作用。
接下来看到的是 COM 模态循环,它因导致重入问题而臭名昭著。注意它是如何调用 PeekMessage() 的,这就是 MouseClick 事件处理程序再次被激活的方式。
这个调用堆栈的惊人之处在于,没有证据表明lock语句转换为调用 CoWaitForMultipleHandles 的代码。它是由 Windows 本身以某种方式完成的,我相当确定 CLR 对此没有任何规定。至少在 SSCLI20 版本中没有。这表明 Windows 实际上有一些关于 CLR 如何实现 Monitor 类的内置知识。非常棒的东西,不知道他们是如何做到的。我怀疑它会修补 DLL 入口点地址以重新向量化代码。
无论如何,这些特殊的对策仅在 NotifyIcon 通知运行时才有效。一种解决方法是延迟事件处理程序的操作,直到回调完成。像这样:
private void notifyIcon1_MouseClick(object sender, MouseEventArgs e) {
this.BeginInvoke(new MethodInvoker(delayedClick));
}
private void delayedClick() {
if (reentrancyDetected) System.Diagnostics.Debugger.Break();
reentrancyDetected = true;
lock (thisLock) {
//do nothing
}
reentrancyDetected = false;
}
问题解决了。