使用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);
}
}
}
现在,如果您开始点击通知图标,则会弹出消息,指示可重入。 我知道为什么STA中的托管等待应该为某些窗口泵送消息的原因。但是我不确定为什么要通知notifyicon的消息。还有一种方法可以在进入/退出方法时避免使用一些布尔指示器进行抽吸吗?
答案 0 :(得分:5)
如果您通过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#
相关功能是CoWaitForMultipleHandles。它确保STA线程无法阻止同步对象,而无需仍然传送消息。哪个非常不健康,因为它很可能导致死锁。特别是在NotifyIcon的情况下,因为阻止通知消息会挂起托盘窗口,使所有图标都不起作用。
接下来你看到的是COM模态循环,臭名昭着导致重入问题。注意它是如何调用PeekMessage()的,这就是MouseClick事件处理程序再次被激活的方式。
这个调用堆栈的惊人之处在于没有证据表明 lock 语句转换为调用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;
}
问题解决了。
答案 1 :(得分:4)
我遇到了同样的问题,你实际上可以通过实现SynchronizationContext并将其设置为当前版本来覆盖所有.NET等待调用的行为。
http://msdn.microsoft.com/en-us/library/system.threading.synchronizationcontext.aspx
如果将IsWaitNotificationRequired属性设置为true,那么只要需要执行等待调用,框架就会调用SynchronizationContext上的Wait方法。
文档有点缺乏,但基本上wait的默认行为是调用CoWaitForMultipleHandles并返回结果。您可以在此处使用适当的标志执行自己的消息泵送和MsgWaitForMultipleObjects,以避免在等待期间调度WM_PAINT。