乍一看像是 MT 问题,但我试图详细了解 COM+ 使用的 STA 模型。

实际上,我有一个用 VB6 编写的遗留 COM+ 组件,它调用用 C++ 编写的本机(即非 COM)Win32 DLL。

遇到一些间歇性(并且在测试中无法重现)问题,我添加了一些调试代码来找出发生了什么,并发现当问题发生时,我在文件中交错了日志消息 - 所以这意味着 DLL同时被两个线程调用。

现在,日志记录将转到基于 _getpid() 和 GetCurrentThreadId() 的每个线程文件,因此,当调用 C++ DLL 中的代码时,它会同时在同一线程上被调用两次。我对 STA 的理解表明这可能是这种情况,因为 COM 将对象的各个实例编组到单个线程上,随意挂起和恢复执行。

不幸的是,我不知道从这里该去哪里。我读到我应该在 DllMain() 中调用 CoInitialiseEx() 来告诉 COM 这是一个 STA DLL,但其他地方说这仅对 COM DLL 有效,并且不会对本机 DLL 产生任何影响。唯一的其他选择是将 DLL 的某些部分包装为关键部分以序列化访问(承受任何性能损失)。

我可以尝试重新设计 DLL,但没有共享状态或全局变量 - 一切都在局部变量中,因此理论上每个调用都应该有自己的堆栈,但我想知道 STA 模型是否基本上对此产生了一些奇怪的影响并且只是在与另一个调用相同的入口点重新进入已加载的 DLL。不幸的是,我不知道如何证明或测试这个理论。

问题基本上是:

  1. 当sta com+组件调用本机DLL时,Sta模型中没有什么可以防止悬挂活动的“线程”并在DLL调用中间传递到另一个“线程”吗?
  2. CoInitialiseEx() 是否是解决此问题的正确方法?
  3. 如果(1)或(2)都不是“好的”假设,那么会发生什么?
有帮助吗?

解决方案

在单元线程 COM 服务器中,保证 COM 类的每个实例都可以由单个线程访问。这意味着 实例 是线程安全的。但是,可以使用不同的线程同时创建许多实例。现在,就 COM 服务器而言,您的本机 DLL 不必执行任何特殊操作。想想 kernel32.dll,每个可执行文件都会使用它 - 当 COM 服务器使用它时,它会初始化 COM 吗?

从 DLL 的角度来看,您必须确保线程安全,因为不同的实例可以同时调用您。在这种情况下,STA 不会保护您。既然你说你没有使用任何全局变量,我只能假设问题出在其他地方,并且恰好在似乎指向 COM 内容的情况下出现。您确定没有一些普通的旧 C++ 内存问题吗?

其他提示

我怀疑你的问题是在被调用的 DLL 深处的某个地方,它对另一个单元(同一进程中的另一个线程,或者 MTA 中的一个对象,或者完全是另一个进程)进行了出站 COM 调用。COM 允许等待出站调用结果的 STA 线程接收另一个入站调用,并递归地处理它。它仅适用于同一对象之间正在进行的对话 - 即A 调用 B,B 回调 A,A 再次回调 B - 但如果您已将接口指针分发给多个客户端,或者客户端已将接口指针共享给另一个客户端,则可以接收来自其他对象的调用。通常,将指向单线程对象的接口指针分发给多个客户端线程是一个坏主意,因为它们只需要互相等待。每个线程创建一个工作对象。

COM 无法在任何线程上随意挂起和恢复执行 - STA 线程上的新传入调用只能通过消息泵到达。当“阻塞”等待响应时,STA 线程实际上正在泵送消息,并使用消息过滤器(请参阅 IMessageFilter)检查是否应处理该消息。但是,消息处理程序不得发出新的传出呼叫 - 如果这样做,COM 将返回 RPC_E_CANTCALLOUT_INEXTERNALCALL 错误(“在消息过滤器内呼出是非法的。”)

如果本机 DLL 中的任何位置都有消息泵 (GetMessage/DispatchMessage),则可能会出现类似的问题。我在界面过程中遇到了 VB 的 DoEvents 问题。

CoInitializeEx 只能由线程的创建者调用,因为只有他们知道他们的消息泵送行为是什么。如果您尝试在 DllMain 中调用它,它很可能会失败,因为您的本机 DLL 正在被调用以响应 COM 调用,因此调用者最终必须已经在线程上调用了 CoInitializeEx 才能进行调用。对于新创建的线程,在 DLL_THREAD_ATTACH 通知中执行此操作可能会在表面上起作用,但如果 COM 在应该泵送时发生阻塞,则会导致程序出现故障,反之亦然。

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top