c# - 带管理等待的 Gui 重入

标签 c# multithreading user-interface reentrancy

我在使用 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 中的托管等待应该为某些窗口发送消息的原因。但我不确定为什么 notifyicon 的消息会被抽走。还有一种方法可以避免在进入/退出方法时不使用某些 bool 指示符而进行泵送吗?

最佳答案

如果您将 MessageBox.Show 调用替换为 Debugger.Break 并附加一个在中断命中时启用 native 调试的调试器,您可以看到发生了什么。调用堆栈如下所示:

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;
    }

问题已解决。

关于c# - 带管理等待的 Gui 重入,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/3650571/

相关文章:

c# - 使用C#在页面上编写ASP代码

javascript - 如何修复或摆脱 ASP.NET Razor 应用程序中的 ESLint 定义要求?

c# - 如何使用 T-SQL 插入 Identity Server 4 持久化的 ApiSecret 值

java - 多线程间的共享数据和独有数据

Java 线程 Random.nextLong() 返回相同的数字

java - Netbeans GUI 和非静态方法

c# - 在单元测试中检查控制台输出

winforms - 显示和隐藏面板(Powershell GUI)

android - 如何只设置圆圈大小(在xml中)?

由于单独的线程,Javafx 无法附加上下文菜单