Gui重新管理等待

我在使用NotifyIcons时发现了一个重入问题。 重现起来非常简单,只需在表单上删除NotiftIcon,click事件应如下所示:

private bool reentrancyDetected; private void notifyIcon1_MouseClick(object sender, MouseEventArgs e) { if (reentrancyDetected) MessageBox.Show("Reentrancy"); reentrancyDetected = true; lock (thisLock) { //do nothing } reentrancyDetected = false; } 

同时启动后台线程会导致一些争用:

 private readonly object thisLock = new object(); private readonly Thread bgThread; public Form1() { InitializeComponent(); bgThread = new Thread(BackgroundOp) { IsBackground = true }; bgThread.Start(); } private void BackgroundOp() { while (true) { lock (thisLock) { Thread.Sleep(2000); } } } 

现在,如果您开始单击notifyicon,将弹出消息,指示重入。 我知道为什么STA中的托管等待应该为某些窗口提取消息的原因。 但是我不确定为什么通知的消息被抽出来了。 还有一种方法可以在进入/退出方法时避免使用一些布尔指示器进行抽吸吗?

如果您通过Debugger.Break替换MessageBox.Show调用并且在中断命中时附加调试器并启用本机调试,则可以看到发生了什么。 调用堆栈如下所示:

 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# 

相关function是CoWaitForMultipleHandles。 它确保STA线程无法阻止同步对象,而无需仍然传送消息。 这是非常不健康的,因为它很可能导致死锁。 特别是在NotifyIcon的情况下,因为阻止通知消息会挂起托盘窗口,使所有图标都不起作用。

你接下来看到的是COM模态循环,臭名昭着导致重入问题。 注意它是如何调用PeekMessage()的,这就是MouseClick事件处理程序再次被激活的方式。

这个调用堆栈的惊人之处在于没有证据表明锁定语句转换为调用CoWaitForMultipleHandles的代码。 它以某种方式由Windows本身完成,我很确定CLR没有任何条款。 至少不在SSCLI20版本中。 它表明Windows实际上有一些关于CLR如何实现Monitor类的内置知识。 非常棒的东西,不知道他们如何能够做到这一点。 我怀疑它修补DLL入口点地址以揭示代码。

Anyhoo,这些特殊的反措施仅在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; } 

问题解决了。

我遇到了同样的问题,你实际上可以通过实现SynchronizationContext并将其设置为当前版本来覆盖所有.NET等待调用的行为。

http://msdn.microsoft.com/en-us/library/system.threading.synchronizationcontext.aspx

如果将IsWaitNotificationRequired属性设置为true,那么只要需要执行等待调用,框架就会调用SynchronizationContext上的Wait方法。

文档有点缺乏,但基本上wait的默认行为是调用CoWaitForMultipleHandles并返回结果。 您可以在此处使用适当的标志执行自己的消息泵送和MsgWaitForMultipleObjects,以避免在等待期间调度WM_PAINT。