10

我在 VS2005 中用 C#、.NET 3.0 编写了一个应用程序,其功能是监视各种可移动驱动器(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 相关的问题,它就在这里。好的,我可以认为调用 WMI 意味着为底层 COM 组件创建一个单独的线程 - 但我仍然没有想到为什么在按下按钮后调用它时需要 no-magic 而调用时需要 do-magic它来自 WndProc。

我对此感到非常困惑,并希望对此事做出澄清。只有一些比有解决方案但不知道它为什么起作用更糟糕的事情:/

干杯,亚历山大

4

1 回答 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 字节
系统.Windows.Forms.dll!System.Windows.Forms.NativeWindow.DebuggableCallback(System.IntPtr hWnd, int msg, System.IntPtr wparam, System.IntPtr lparam) + 0x64 bytes [Native to Managed Transition]
[Managed to Native Transition]
mscorlib.dll!System.Threading.WaitHandle.InternalWaitOne(System.Runtime.InteropServices.SafeHandle waitableSafeHandle, long millisecondsTimeout, bool hasThreadAffinity, bool exitContext) + 0x2b bytes 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 类型) + 0x17b 字节
System.Management.dll!System。 Management.ManagementPath.CreateWbemPath(string path) + 0x18 bytes System.Management.dll!System.Management.ManagementClass.ManagementClass(string path) + 0x29 bytes
DeleteMeWindowsForms.exe!DeleteMeWindowsForms.Form1.GetDrives() Line 23 + 0x1b bytes C#

如此有效地泵送窗口消息以确保正确编组 COM 调用,但这具有再次调用 WndProc 和 GetDrives 的副作用(因为有待处理的 WM_DEVICECHANGE 消息),而仍在先前的 GetDrives 调用中。当您使用 BeginInvoke 时,您将删除此递归调用。

同样,在 GetDrives 调用上放置一个断点,并在第一次命中后按 F5。下一次,等待一两秒钟,然后再次按 F5。有时它会失败,有时它不会,你会再次遇到断点。这一次,您的调用堆栈将包括对 GetDrives 的三个调用,最后一个调用由 diskDriveList 集合的枚举触发。因为再次发送消息以确保调用被编组。

很难准确指出触发MDA的原因,但考虑到递归调用,可以合理地假设 COM 上下文可能被过早地拆除和/或在底层 COM 对象可以释放之前收集对象。

于 2011-04-14T02:02:34.093 回答