MTA控制台应用程序从多个线程调用STA COM对象

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

  • 控制台应用程序,默认为Multi-Threaded Apartment(Main()显式具有[MTAThread]属性)。
  • 主线程产生一些工作线程。
  • 主线程实例化单线程COM对象。
  • 主线程调用Console.ReadLine()直到用户点击’q’,然后应用程序终止。

几个问题:

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

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

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

这种情况通常是不可取的。 它有很多缺点,如果COM无法封送对象的某些接口,可能根本无法正常工作。 查看此问题了解更多详情。 此外,由隐式STA公寓运行的消息泵循环仅泵送有限数量的COM特定消息。 这也可能会影响COM的function。

你可以尝试一下,它可能适合你。 或者,您可能遇到一些令人不快的问题,如死锁,很难诊断。

这是我刚才回答的一个密切相关的问题:

StaTaskScheduler和STA线程消息泵送

我个人更喜欢手动控制线程间调用和线程关联的逻辑,比如在我的回答中提出的ThreadAffinityTaskScheduler

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

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

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

最重要的是,COM组件上的每个调用都将被编组。 这很慢,编组调用通常比直接调用一个本身只花费很少时间的方法慢10,000倍。 就像一个属性getter call。 这当然可以让你的程序陷入困境。

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

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

因此,即使没有调用Application.Run(),将[STAThread]放在Main()方法上也可能足以获得快乐的快速代码。 只要记住后果,看到一个方法调用死锁或一个没有被提升的事件就是需要抽水的标志。

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

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

或者CLR会在新的STA线程上为我创建它

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

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

要看。

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

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

在性能方面更好

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

(记住,如果你为STA创建线程然后在本地实例化你需要做消息泵。我不清楚是否有任何CLR级轻量级消息泵 – 包括WinForms只是为了这个肯定不是。 )

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