我一直在使用第三方 SDK 作为 C# 中的引用 dll,在 Windows 10 上使用 .NET 4.5.2。IDE 围绕 dll 创建了一个互操作,我可以看到适当的命名空间、接口、枚举等。我正在使用的 SDK 适用于 Blackmagic ATEM Television Studio,但我认为这没有直接关系。
SDK 提供了一个对象,我可以使用它来定位/发现附加的硬件并返回一个对象的实例,它似乎是底层 COM 对象的包装器 (RCW)。
发现代码如下所示:
string address = "192.168.1.240";
_BMDSwitcherConnectToFailure failureReason = 0;
IBMDSwitcher switcher = null;
var discovery = new CBMDSwitcherDiscovery();
discovery.ConnectTo(address, out switcher, out failureReason);
如果我在作为 STA 单元的 WPF UI 线程上执行此代码,它将毫无问题地工作。我能够毫无错误地使用该对象。但是,如果我尝试从任何新线程(STA 或 MTA)访问生成的切换器对象,我将收到类似于以下内容的错误:
无法将类型为“System.__ComObject”的 COM 对象转换为接口类型“BMDSwitcherAPI.IBMDSwitcherMixEffectBlock”。此操作失败,因为 IID 为“{11974D55-45E0-49D8-AE06-EEF4D5F81DF6}”的接口的 COM 组件上的 QueryInterface 调用因以下错误而失败:不支持此类接口(来自 HRESULT 的异常:0x80004002 (E_NOINTERFACE)) .
引用 MikeJ 和他对类似问题的回答:
这个讨厌的、讨厌的异常是由一个称为 COM 编组的概念引起的。问题的本质在于,为了从任何线程消费 COM 对象,线程必须能够访问描述 COM 对象的类型信息。
在这一点上,我决定稍微调整我的策略并尝试从 MTA 线程中调用发现逻辑。像这样:
private IBMDSwitcher _switcher = null;
public void Connect()
{
Task.Run(() =>
{
string address = "192.168.1.240";
IBMDSwitcherDiscovery discovery = new CBMDSwitcherDiscovery();
_BMDSwitcherConnectToFailure failureReason = 0;
discovery.ConnectTo(address, out _switcher, out failureReason);
});
}
这似乎有效,但需要付出代价。
权衡是我现在可以从任何后台 (MTA) 线程毫无问题地使用我的切换器对象,但我的整个应用程序在运行大约 15 分钟后将简单地崩溃。在有人说之前,我知道这不是线程安全的。我认识到在多个 MTA 线程尝试使用我的切换器对象之前需要一些同步逻辑。但那是在路上,现在如果我运行上面的代码并简单地让应用程序空闲 15 分钟,它每次都会崩溃。
因此,一方面我有一个稳定的应用程序,但我只能使用 UI 线程中的库,另一方面我有一个不稳定的应用程序,但我可以使用任何(MTA)后台线程中的库。
由于我的应用程序的性质,最好能够从多个后台线程使用该库。例如,一个线程可能正在生成图形并将其上传到设备,而另一个线程正在管理切换视频输入,第三个线程正在管理音频输入,而 UI 线程正在处理 ID-10T bonk 对话框......所有这些可以从 UI 线程完成,但如果可能的话,我真的宁愿避免这种情况。
所以:有什么方法可以在 UI(STA)线程上发现并创建我的切换器对象(理论上避免在 15 分钟内爆炸),但让我的后台(MTA)线程可以访问类型信息,这样他们也可以使用切换器?或者,在后台(MTA)线程上发现/创建我的切换器对象是否安全,但我需要做一些事情以某种特殊方式“标记”它以避免 15 分钟的爆炸?
编辑:
在阅读了 Hans Passant 的评论并调查了他提供的链接后,我改编了引用的 STAThread 类以供我自己在 WPF 中使用。但我对 SynchronizationContext 和 Dispatcher 有点模糊。下面列出的代码是否可以安全地用作“围绕”我的 COM 对象的包装器,而不会在任何“长时间运行”操作期间阻塞 UI?我的用例是运行一个 Task() 来准备一些数据,然后调用一些代码与我的 COM 对象进行交互,当操作完成时,代码流在正在运行的 Task 中恢复以执行下一系列操作。 .
public class STAThread
{
private Thread thread;
private SynchronizationContext ctx;
private ManualResetEvent mre;
public STAThread()
{
using (mre = new ManualResetEvent(false))
{
thread = new Thread(() =>
{
ctx = new SynchronizationContext();
mre.Set();
Dispatcher.Run();
});
thread.IsBackground = true;
thread.SetApartmentState(ApartmentState.STA);
thread.Start();
mre.WaitOne();
}
}
public void BeginInvoke(Delegate dlg, params Object[] args)
{
if (ctx == null) throw new ObjectDisposedException("STAThread");
ctx.Post((_) => dlg.DynamicInvoke(args), null);
}
public object Invoke(Delegate dlg, params Object[] args)
{
if (ctx == null) throw new ObjectDisposedException("STAThread");
object result = null;
ctx.Send((_) => result = dlg.DynamicInvoke(args), null);
return result;
}
}