我在实用程序库中有一个问题,它有一些COM互操作.它保留对调用之间使用的COM对象的引用.
如果使用相同的COM线程模型从线程调用所有方法,则该类可以正常工作.
但是,如果创建COM对象的调用使用与后续调用不同的线程模型,则QueryInterface将失败E_NOINTERFACE.
当我们async在单元测试中添加分支时,我们才发现这一点; 在此之前它在所有MTA应用程序中运行良好所有STA单元测试...
我想我理解失败的原因(通过COM文档,Chris Brumme的博客) - 正在使用的COM对象支持"两个"线程模型,这导致C#在STA和MTA创建的实例之间创建一个范围.
但是从图书馆的角度来看,我能想到的唯一修复方法有点垃圾:
CurrentThread.ApartmentState)有没有更干净/更容易的选择?这是一个MCVE:
class Program
{
[ComImport, Guid("62BE5D10-60EB-11d0-BD3B-00A0C911CE86")] class SystemDeviceEnum { };
[ComVisible(true), ComImport, Guid("29840822-5B84-11D0-BD3B-00A0C911CE86"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface ICreateDevEnum { [PreserveSig] int CreateClassEnumerator([In] ref Guid pType, [Out] out IEnumMoniker ppEnumMoniker, [In] int dwFlags); }
static ICreateDevEnum createDeviceEnum;
static Guid VideoInputDeviceCategory = new Guid("860BB310-5D01-11d0-BD3B-00A0C911CE86");
static void Prepare()
{
var coSystemDeviceEnum = new SystemDeviceEnum();
createDeviceEnum = (ICreateDevEnum)coSystemDeviceEnum;
}
static int GetDeviceCount()
{
IEnumMoniker enumMoniker;
createDeviceEnum.CreateClassEnumerator(ref VideoInputDeviceCategory, out enumMoniker, 0);
if (enumMoniker == null) return 0;
int count = 0;
IMoniker[] moniker = new IMoniker[1];
while (enumMoniker.Next(1, moniker, IntPtr.Zero) == 0) count++;
return count;
}
[STAThread]
static void Main(string[] args)
{
RunTestAsync().Wait();
}
private static async Task RunTestAsync()
{
Prepare();
await Task.Delay(1);
var count = GetDeviceCount();
Console.WriteLine(string.Format("{0} video capture device(s) found", count));
}
}
Run Code Online (Sandbox Code Playgroud)
众所周知,COM线程很难理解.其实不多,多容易得走量比线程.NET类.几乎每个人都知道,例如,List <>或Random类不是线程安全的.并不是很多人都知道如何以线程安全的方式使用它们.COM设计者有更高的目标,并假设程序员一般不知道如何编写线程安全的代码,而智能人员应该处理它.
它需要处理一些细节.首先,您必须告诉COM您愿意为不是线程安全的coclass提供什么样的支持,但无论如何都要从工作线程使用.在那里你犯了一个可怕的,可怕的罪行.当你使用[STAThread]然后你做出承诺.你必须做的两件事:你必须永远不要阻止线程,你必须抽一个消息循环(又名Application.Run).请注意你是如何破坏这两个要求 永远不要撒谎,当你这么做时会发生非常糟糕的事情.但你还没有那么远.
您可以从您正在使用的coclass中获得的线程支持类型很容易被发现.启动Regedit.exe并导航到HKLM\Software\Wow6432Node\Classes\CLSID.找到您使用的{guid}并查看您在InProcServer32密钥中看到的ThreadingModel值.对于您正在使用的那个,它是"两者".意味着它是从STA线程和根本不支持线程安全的线程编写的,并在MTA中运行.就像你的主线程和你的任务一样.正如您所发现的,它可以正常工作.请注意,这不是很平常,绝大多数COM服务器只支持"Apartment"线程模型.微软通常需要额外的千里才能支持两者.
因此,您在STA线程上创建了枚举器对象,并在MTA中的线程上使用它.现在COM运行时必须做一些非常重要的事情,它必须确保可能从您调用的方法调用的任何回调(aka事件)在同一个STA线程上运行,以便回调中的任何代码都是线程安全的好.换句话说,它必须将来自工作线程的调用封送回主线程.相当于.NET应用程序中的Control.Invoke或Dispatcher.Invoke.在COM中完全自动完成.
这需要在.NET中做一些非常简单但在非托管代码中非常困难的事情.必须将方法的参数从一个堆栈帧复制到另一个堆栈帧,以便可以在另一个线程上进行调用.借助Reflection可以轻松完成.NET.对于非托管代码来说,这并不容易,它需要一个知道方法参数类型是什么的oracle,它是缺少元数据的替代品.
该oracle也可以在注册表中找到.使用Regedit并导航到HKLM\Software\Wow6432Node\Classes\Interface键.正如异常消息所示,找到那里的接口guid,{29840822-5B84-11D0-BD3B-00A0C911CE86}.你会注意到这个问题:它不存在.是的,异常消息非常糟糕.在实际 E_NOINTERFACE了解,由于COM运行时无法找到任何其他方式,IMarshal不支持.如果它会在那里,那么你将处理[STAThread]谎言,你的线程将陷入僵局.
这是不寻常的顺便说一句,使用"Both"的ThreadingModel的COM对象模型几乎总是支持编组.只是不是你想要使用的特定的.DirectShow在过去10年中已被弃用,取而代之的是Media Foundation.你找到了微软决定退休的一个很好的理由.
所以这只是你需要知道的事情.与必须知道Random类不是线程安全的细节没有什么不同.它在MSDN中没有很好的文档,但如上所述,它很容易自己发现.