一种MTA控制台应用程序调用多个线程的STA COM对象。

13

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

  • 一个控制台应用程序,默认情况下为多线程公寓(Main() 显式具有 [MTAThread] 特性)。
  • 主线程生成一些工作线程。
  • 主线程实例化单线程COM对象。
  • 主线程调用 Console.ReadLine() 直到用户按下 'q',此后应用程序终止。

几个问题:

  • 许多地方提到了 COM 对象需要消息泵的需求。我是否需要手动为主线程创建消息泵,或者CLR会像这里的问题建议的那样,为我在新的STA线程上创建它?
  • 只是为了确保——假设CLR自动创建必要的管道,那么我是否可以在任何工作线程中使用COM对象而无需显式同步?
  • 以下哪种方法在性能方面更好:
    • 让CLR处理与COM对象之间的编组。
    • 在单独的STA线程上明确实例化对象,并使其他线程通过例如ConcurrentQueue与之通信。

3个回答

13
这是由COM自动执行的。由于您的COM对象是单线程的,因此COM需要一个合适的主机来确保它以线程安全的方式使用。由于您的主线程无法提供这样的保证,COM会自动创建另一个线程并在该线程上创建对象。此线程还会自动泵,无需任何帮助。您可以在调试器中看到它被创建。启用非托管调试并查看调试+窗口+线程窗口。当您跨越新的调用时,您将看到线程正在增加。
很简单,但确实有一些后果。首先,COM组件需要提供代理/存根实现。辅助代码知道如何序列化方法调用的参数,以便在另一个线程上进行真正的方法调用。通常会提供,但不总是。如果缺少它,您将获得难以诊断的E_NOINTERFACE异常。有时是TYPE_E_LIBNOTREGISTERED,这是常见的安装问题。
最重要的是,对COM组件的每个调用都将进行封送处理。这很慢,一个封送调用通常比直接调用一个需要很少时间的方法(如属性getter调用)慢约10,000倍。当然,这可能会使您的程序变慢。
STA线程避免了这种情况,因此是使用单线程组件的推荐方式。是的,STA线程需要泵消息循环。在.NET程序中的Application.Run()。正是消息循环在COM中使调用从一个线程封送到另一个线程。请注意,这并不一定意味着您必须具有消息循环。如果没有调用需要封送处理,换句话说,如果您从同一线程上进行了所有调用,则不需要消息循环。通常很容易保证,特别是在控制台模式应用程序中。除非您自己创建线程。

还有一个令人讨厌的细节:单线程的COM组件有时会假设它是在使用消息泵的线程上创建的。并且通常会使用PostMessage()自己,特别是在内部使用工作线程并需要在STA线程上引发事件时。当您不使用消息泵时,这当然将无法正常工作。您通常通过注意到未引发事件来诊断此问题。这样组件的共同示例是WebBrowser。它在内部大量使用线程,但在创建时在该线程上引发事件。如果您不使用消息泵,则永远不会获得DocumentCompleted事件。

因此,在您的Main()方法中放置[STAThread]可能足以获得快乐的快速代码,即使没有调用Application.Run()。只需记住后果,看到方法调用死锁或事件未被引发是需要使用消息泵的标志。


1
非常好的答案,谢谢。您提到如果我从主线程进行所有调用,则可能不需要泵。主线程有什么特别之处?它与在另一个STA线程上创建对象并从该线程进行所有调用有何不同? - bavaza
2
唯一重要的是方法调用是否由创建对象的线程进行。这总是线程安全的。如何使其成为STA确实很重要。主线程是特殊的,Windows在启动进程时创建它。因此需要[STAThread]属性。您启动的任何线程都需要Thread.SetApartmentState()。 - Hans Passant
我还是很困惑。如果我有一个使用dll的asmx服务 - 在这个dll中,我创建了一个单线程公寓,它执行MQ泵送并完成其任务。这意味着每个并发的webservice请求都应该创建自己的实例,并拥有自己的STA线程 - COM对象应该正常工作。在大负载下,COM对象代码会锁定,我们每次都会超时WS调用。需要重新启动才能恢复功能 - 这让我认为WebBrowser只能有一个实例。在单次失败后,随后的请求会堆积。 - Iofacture
有趣的是,这几乎完美地描述了我的问题 - 但我们已经在代码中实现了这个解决方案,并遇到了相同的行为:https://blogs.msdn.microsoft.com/asiatech/2012/02/20/we-may-experience-performance-issue-when-wcf-service-communicates-with-sta-com/ - Iofacture

5
可以在MTA线程中创建一个STA COM对象。在这种情况下,COM(而不是CLR)将创建一个隐式的STA单元(一个独立的由COM拥有的线程)或重用先前创建的单元。对象将在那里实例化,然后为其创建一个线程安全的代理对象(COM封送包装器),并将其返回到MTA线程。在MTA线程上对对象进行的所有调用都将由COM封送到该隐式STA单元。这种情况通常是不可取的。它有很多缺点,如果COM无法封送对象的某些接口,则可能根本无法按预期工作。请参考this question了解更多详细信息。此外,被隐式STA单元运行的消息泵循环仅抽取一定数量的COM特定消息。这也可能影响COM的功能性。您可以尝试它,它可能适合您。或者,您可能会遇到一些不愉快的问题,例如死锁,难以诊断。这是一个与之密切相关的问题,我最近回答过:

StaTaskScheduler和STA线程消息泵

我个人更喜欢手动控制跨线程调用和线程亲和性的逻辑,例如我回答中提出的ThreadAffinityTaskScheduler

你也可以阅读这个:INFO:OLE线程模型的说明和工作原理,强烈推荐。


5

我需要手动为主线程创建消息泵吗?

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

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

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

可以从任何工作线程访问COM对象而无需显式同步。

这取决于情况。

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

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

就性能而言,哪个更好?

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

(请记住,如果您为STA创建线程,然后本地实例化对象,则需要进行消息泵处理。对于此,我不清楚是否有任何CLR级别的轻量级消息泵 - 包括WinForms肯定没有。)

NB. COM和CLR的详细解释只有Adam Nathan(Sams,2002年1月)的.NET and COM: The Complete Interoperability Guide。但此书基于.NET 1.1版本,已经停印(但有Kindle版,并可通过Safari Books Online获取)。即使这本书也没有直接描述您正在尝试做的事情。我建议进行一些原型设计。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接