Gui重新管理等待

时间:2010-09-06 10:04:06

标签: c# multithreading user-interface reentrancy

使用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的消息。还有一种方法可以在进入/退出方法时避免使用一些布尔指示器进行抽吸吗?

2 个答案:

答案 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。