在单线程应用程序中调用WMI函数时出现了DisconnectedContext MDA问题

10
我用C#编写了一个应用程序,使用.NET 3.0在VS2005中实现了监控各种可移动驱动器(USB闪存盘、CD-ROM等)插入/弹出的功能。我不想使用WMI,因为它有时会产生歧义(例如,对于一个USB驱动器,它可以生成多个插入事件),所以我简单地重写了主窗体的WndProc来捕获WM_DEVICECHANGE消息,如此处所提议的那样。昨天,当我发现我必须使用WMI检索一些模糊的磁盘详细信息(比如序列号)时,我遇到了一个问题。结果发现,从WndProc内部调用WMI例程会引发DisconnectedContext MDA异常。
经过一些挖掘,我最终采用了一个尴尬的解决方法。代码如下:
    // the function for calling WMI 
    private void GetDrives()
    {
        ManagementClass diskDriveClass = new ManagementClass("Win32_DiskDrive");
        // THIS is the line I get DisconnectedContext MDA on when it happens:
        ManagementObjectCollection diskDriveList = diskDriveClass.GetInstances();
        foreach (ManagementObject dsk in diskDriveList)
        {
            // ...
        }
    }

    private void button1_Click(object sender, EventArgs e)
    {
        // here it works perfectly fine
        GetDrives();
    }


    protected override void WndProc(ref Message m)
    {
        base.WndProc(ref m);

        if (m.Msg == WM_DEVICECHANGE)
        {
            // here it throws DisconnectedContext MDA 
            // (or RPC_E_WRONG_THREAD if MDA disabled)
            // GetDrives();
            // so the workaround:
            DelegateGetDrives gdi = new DelegateGetDrives(GetDrives);
            IAsyncResult result = gdi.BeginInvoke(null, "");
            gdi.EndInvoke(result);
        }
    }
    // for the workaround only
    public delegate void DelegateGetDrives();

这基本上意味着在单独的线程上运行与WMI相关的过程,但是需要等待它完成。

现在的问题是:为什么它有效,为什么必须这样做?(或者说,是否必须这样做?)

我不明白首先会出现DisconnectedContext MDA或RPC_E_WRONG_THREAD的原因。从按钮单击事件处理程序运行GetDrives()过程与从WndProc调用它有何区别?它们不是发生在我的应用程序的同一主线程上吗?顺便说一下,我的应用程序完全是单线程的,所以为什么突然会出现涉及某些“错误线程”的错误?使用WMI是否意味着多线程和对System.Management函数的特殊处理?

同时,我找到了另一个与MDA相关的问题,here。好吧,我可以认为调用WMI意味着为基础COM组件创建单独的线程,但我仍然不明白为什么按下按钮后调用它时不需要魔法,而从WndProc调用它时需要魔法。

我真的对此感到困惑,并希望能够澄清这个问题。只有几件比拥有解决方案但不知道为什么它有效的事情更糟糕:/

干杯, Aleksander


1
我也有同样的问题!希望能找到解决方案。我会添加赏金...或许这会有所帮助。 - Brad
1个回答

6

这里有一个相当长的关于COM公寓和消息泵 在这里 的讨论。但主要感兴趣的是消息泵被用来确保在STA中调用时正确地进行封送处理。由于UI线程是涉及的STA,需要抽取消息以确保一切正常运作。

WM_DEVICECHANGE消息实际上可以多次发送到窗口。因此,在直接调用GetDrives的情况下,您实际上会得到递归调用。在GetDrives调用上放置断点,然后连接设备以触发事件。

第一次命中断点时,一切都很好。现在按F5继续,您将再次命中断点。这次调用堆栈大致如下:

[在睡眠、等待或加入状态中] DeleteMeWindowsForms.exe!DeleteMeWindowsForms.Form1.WndProc(ref System.Windows.Forms.Message m) 第46行 C# System.Windows.Forms.dll!System.Windows.Forms.Control.ControlNativeWindow.OnMessage(ref System.Windows.Forms.Message m) + 0x13 字节
System.Windows.Forms.dll!System.Windows.Forms.Control.ControlNativeWindow.WndProc(ref System.Windows.Forms.Message m) + 0x31 字节
System.Windows.Forms.dll!System.Windows.Forms.NativeWindow.DebuggableCallback(System.IntPtr hWnd, int msg, System.IntPtr wparam, System.IntPtr lparam) + 0x64 字节 [从本机到托管的转换]
[从托管到本机的转换]
mscorlib.dll!System.Threading.WaitHandle.InternalWaitOne(System.Runtime.InteropServices.SafeHandle waitableSafeHandle, long millisecondsTimeout, bool hasThreadAffinity, bool exitContext) + 0x2b 字节 mscorlib.dll!System.Threading.WaitHandle.WaitOne(int millisecondsTimeout, bool exitContext) + 0x2d 字节
mscorlib.dll!System.Threading.WaitHandle.WaitOne() + 0x10 字节 System.Management.dll!System.Management.MTAHelper.CreateInMTA(System.Type type) + 0x17b 字节
System.Management.dll!System.Management.ManagementPath.CreateWbemPath(string path) + 0x18 字节 System.Management.dll!System.Management.ManagementClass.ManagementClass(string path) + 0x29 字节
DeleteMeWindowsForms.exe!DeleteMeWindowsForms.Form1.GetDrives() 第23行 + 0x1b 字节 C#

因此,实际上窗口消息正在被泵送以确保COM调用被正确地编组,但这会导致在先前的GetDrives调用中仍存在待处理的WM_DEVICECHANGE消息时再次调用您的WndProc和GetDrives。当您使用BeginInvoke时,您将删除此递归调用。
同样,在GetDrives调用上放置断点,并在第一次命中后按F5。下一次,等待一秒或两秒,然后再次按F5。有时它会失败,有时它不会,您将再次命中断点。这次,您的调用堆栈将包括三个对GetDrives的调用,最后一个由diskDriveList集合的枚举触发。因为再次,为确保调用被编组,消息正在被泵送。
很难确定为什么会触发MDA,但考虑到递归调用,可以合理地假设COM上下文可能会过早地被撤销和/或对象在底层COM对象被释放之前被收集。

我正在慢慢开始理解,请耐心等待。基本上,你是说调用GetDrives()需要在他的窗体上运行WndProc?我不明白这是个问题,特别是因为他首先允许基础处理它。GetDrives()不会再次被调用,因为他首先测试消息类型,对吗?你能再详细解释一下,或者指点我正确的方向吗?对于我的困惑,很抱歉。谢谢! - Brad
1
@Brad - 有多个WM_DEVICECHANGE消息正在发送。因此,第一次调用WndProc时,它处理了这些消息中的第一个。GetDrives调用会按顺序抽取消息,以将任何COM调用封送到STA线程中(例如来自WMI对象的返回值)。由于还有更多的WM_DEVICECHANGE消息等待处理,因此抽取消息队列将强制将它们通过WndProc覆盖推送。因此出现了递归。 - CodeNaked
1
@CodeNaked,我明白了。通常情况下,当我在WndProc中执行某个操作时,它会阻塞直到操作完成,然后再为队列中的其他消息调用WndProc。对于WMI对象的封送,需要使用WndProc。所以如果我在WndProc中使用WMI,就会遇到问题,因为WndProc被阻塞了,但是WMI需要它运行才能正常工作。我的总结大致正确吗?再次感谢。现在我明白解决方案是不在WndProc中使用任何需要封送的内容。 - Brad
1
@Brad - 你说得对。通常情况下,事情会被阻塞,但这种情况下,技术上是阻塞了第一个循环来泵送消息队列,但由于COM封送,另一个循环会启动来泵送队列。 - CodeNaked
@CodeNaked,太酷了,谢谢!我在https://dev59.com/w1bTa4cB1Zd3GeqP8jMh也遇到了类似的问题,我将使用Aleksander的方法来解决它,或者采用其他异步方式。 - Brad
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接