12

尽管有很多关于 COM 和 STA/MTA 的问题(例如这里),但其中大多数都在谈论具有 UI 的应用程序。但是,我有以下设置:

  • 一个控制台应用程序,默认情况下是多线程单元(Main() 显式具有该[MTAThread]属性)。
  • 主线程产生一些工作线程。
  • 主线程实例化一个单线程 COM 对象。
  • 主线程调用 Console.ReadLine() 直到用户点击“q”,之后应用程序终止。

几个问题:

  • 许多地方都提到了COM 对象需要消息泵。我是否需要为主线程手动创建一个消息泵,或者 CLR 会在新的 STA 线程上为我创建它,正如这个问题所暗示的那样?
  • 只是为了确保 - 假设 CLR 自动创建必要的管道,然后我可以使用来自任何工作线程的 COM 对象而无需显式同步吗?
  • 以下哪项在性能方面更好:
    • 让 CLR 负责与 COM 对象之间的封送处理。
    • 在单独的 STA 线程上显式实例化对象,并让其他线程通过例如ConcurrentQueue.
4

3 回答 3

12

这是由 COM 自动完成的。由于您的 COM 对象是单线程的,因此 COM需要为对象提供一个合适的主目录,以确保它以线程安全的方式使用。由于您的主线程不够友好,无法提供此类保证,COM 会自动创建另一个线程并在该线程上创建对象。这个线程也会自动抽水,你无需做任何帮助。您可以看到它正在调试器中创建。启用非托管调试并查看 Debug + Windows + Threads 窗口。当您跳过呼叫时,您会看到线程被添加。

很好很容易,但它确实有一些后果。首先,COM 组件需要提供代理/存根实现。帮助代码知道如何序列化方法调用的参数,以便可以在另一个线程上进行真正的方法调用。通常会提供,但并非总是如此。如果 E_NOINTERFACE 异常丢失,您将很难诊断它。有时 TYPE_E_LIBNOTREGISTERED,一个常见的安装问题。

最重要的是,对 COM 组件的每次调用都会被封送。这很慢,编组调用通常比直接调用本身花费很少时间的方法慢 10,000 倍左右。就像一个属性 getter 调用。当然,这确实会使您的程序陷入困境。

STA 线程避免了这种情况,因此是使用单线程组件的推荐方法。是的,STA 线程需要泵送消息循环。.NET 程序中的 Application.Run()。它是消息循环,它将 COM 中从一个线程到另一个线程的调用编组。请注意,这并不一定意味着您必须有一个消息循环。如果没有调用需要编组,或者换句话说,如果您从同一个线程对组件进行所有调用,则不需要消息循环。这通常很容易保证,尤其是在控制台模式应用程序中。当然,如果您自己创建线程,则不会。

一个更令人讨厌的细节:单线程 COM 组件有时假定它是在泵的线程上创建的。并且将使用 PostMessage() 本身,通常在它在内部使用工作线程并需要在 STA 线程上引发事件时。当您不抽水时,这当然不会再正常工作了。您通常通过注意到没有引发事件来诊断这一点。此类组件的常见示例是 WebBrowser。它在内部大量使用线程,但在创建它的线程上引发事件。如果您不抽水,您将永远不会收到 DocumentCompleted 事件。

因此,将 [STAThread] 放在 Main() 方法上可能足以获得快乐的快速代码,即使没有调用 Application.Run()。请记住后果,看到方法调用死锁或未引发事件是需要抽水的迹象。

于 2014-01-30T15:08:26.543 回答
5

我是否需要为主线程手动创建一个消息泵,

不。它在 MTA 中,因此不需要消息泵。

还是 CLR 会在新的 STA 线程上为我创建它

如果 COM 创建线程(因为进程中没有 STA),那么它也会创建消息泵(和一个隐藏窗口:可以使用 SPY++ 和类似的调试工具看到)。

来自任何工作线程的 COM 对象,无需显式同步

要看。

如果在 MTA 中创建了对单线程对象 (STO) 的引用,那么 COM 将提供适当的代理。此代理适用于 MTA 中的所有线程。

在任何其他情况下,都需要编组引用以确保它具有正确的代理。

在性能方面更好

唯一的答案是测试两者并进行比较。

(请记住,如果您为 STA 创建线程,然后在本地实例化对象,则需要进行消息泵送。我不清楚是否存在任何 CLR 级别的轻量级消息泵——包括仅用于此目的的 WinForms,当然不是。 )

注意。COM 和 CLR 的唯一深入解释性覆盖是.NET 和 COM: Adam Nathan 的完整互操作性指南(Sams,2002 年 1 月)。但它基于 .NET 1.1,现已绝版(但有 Kindle 版本,可通过Safari Books Online获得)。甚至这本书也没有直接描述你想要做什么。我会建议一些原型设计。

于 2014-01-30T10:23:44.163 回答
5

是的,可以从MTA线程创建STA COM 对象。

在这种情况下,COM(不是CLR)将创建一个隐式 STA 单元(一个单独的 COM 拥有的线程)或重新使用现有的、更早创建的单元。COM 对象将在那里被实例化,然后为其创建一个线程安全的代理对象(COM 编组包装器)并返回给 MTA 线程。在 MTA 线程上对对象进行的所有调用都将由 COM 编组到该隐式 STA 单元。

这种情况通常是不可取的。如果 COM 无法编组对象的某些接口,它有很多缺点并且可能根本无法按预期工作。检查此问题以获取更多详细信息。此外,由隐式 STA 单元运行的消息泵循环仅泵送有限数量的 COM 特定消息。这也可能影响 COM 的功能。

您可以尝试一下,它可能对您很有效。或者,您可能会遇到一些令人不快的问题,例如死锁,很难诊断。

这是我最近回答的一个密切相关的问题:

StaTaskScheduler 和 STA 线程消息泵送

我个人更喜欢手动控制线程间调用和线程关联的逻辑,就像ThreadAffinityTaskScheduler我的回答中提出的那样。

您可能还想阅读以下内容:INFO:OLE 线程模型的描述和工作,强烈推荐。

于 2014-01-30T10:21:30.497 回答