3

我在使用 NotifyIcons 时发现了一个重入问题。这真的很容易重现,只需在表单上放置一个 NotiftIcon,点击事件应该如下所示:

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 中的托管等待应该为某些窗口发送消息的原因。但我不确定为什么通知图标的消息会被抽出。还有一种方法可以在进入/退出方法时不使用一些布尔指标来避免抽水吗?

4

2 回答 2

5

如果您将 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;
    }

问题解决了。

于 2010-09-06T11:42:31.017 回答
4

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

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

如果您将 IsWaitNotificationRequired 属性设置为 true,那么框架将在需要执行等待调用的任何时候调用 SynchronizationContext 上的 Wait 方法。

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

于 2011-02-24T20:00:22.657 回答