【问题标题】:An MTA Console application calling an STA COM object from multiple threads从多个线程调用 STA COM 对象的 MTA 控制台应用程序
【发布时间】:2014-02-22 10:48:24
【问题描述】:

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

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

几个问题:

  • 很多地方都提到了need of a message pump for COM objects。我是否需要像this 问题所暗示的那样,为主线程手动创建一个消息泵,或者 CLR 会在一个新的 STA 线程上为我创建它?
  • 只是为了确保 - 假设 CLR 自动创建必要的管道,那么我是否可以在不需要显式同步的情况下从任何工作线程使用 COM 对象?
  • 以下哪项在性能方面更好:
    • 让 CLR 负责与 COM 对象之间的封送处理。
    • 在单独的 STA 线程上显式实例化对象,并让其他线程通过例如ConcurrentQueue

【问题讨论】:

标签: c# com-interop sta mta


【解决方案1】:

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

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

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

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

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

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

【讨论】:

  • 一个很好的答案,谢谢。您提到如果我从主线程进行所有调用,则可能不需要泵。主线程有什么特别之处?它与在另一个 STA 线程上创建对象并从该线程进行所有调用有何不同?
  • 没什么。唯一重要的是方法调用是否由创建对象的线程进行。那总是线程安全的。究竟如何使它成为 STA 确实很重要。主线程是特殊的,Windows 在启动进程时创建它。所以需要 [STAThread] 属性。您启动的任何线程都需要 Thread.SetApartmentState()。
  • 我还是一头雾水。如果我有一个使用 dll 的 asmx 服务 - 在这个 dll 中,我正在创建一个单独的单元线程,它执行 MQ 泵送并完成它。这意味着每个并发的 web 服务请求都应该使用它自己的 STA 线程创建它自己的实例——COM 对象应该运行。大多数情况下它确实如此——但在高负载下,COM 对象代码会锁定,并且我们每次都在 WS 调用上超时。需要重新启动才能恢复功能 - 这让我认为只能有一个 WebBrowser 实例。一次失败后,后续请求就会堆积起来。
  • 有趣的是,这几乎完全准确地描述了我的问题 - 但我们已经在代码中实现了这个解决方案并遇到了相同的行为:blogs.msdn.microsoft.com/asiatech/2012/02/20/…
【解决方案2】:

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

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

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

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

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

StaTaskScheduler and STA thread message pumping

我个人更喜欢手动控制线程间调用和线程亲和性的逻辑,在我的回答中提出了类似ThreadAffinityTaskScheduler 的内容。

您可能还想阅读以下内容:INFO: Descriptions and Workings of OLE Threading Models,强烈推荐。

【讨论】:

    【解决方案3】:

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

    没有。它位于 MTA 中,因此不需要消息泵。

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

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

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

    视情况而定。

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

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

    在性能方面更好

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

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

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2015-03-03
      • 2019-12-11
      • 2011-01-11
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多