传统 VB6 COM+ DLL 调用本机 Win32 DLL — STA 的线程问题?
乍一看像是 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。 不幸的是,我不知道如何证明或测试这个理论。
问题基本上是:
- 当 STA COM+ 组件调用本机 DLL 时,STA 模型中没有任何内容可以 防止活动“线程”在 DLL 调用过程中被挂起并将控制权传递给另一个“线程”?
- CoInitialiseEx() 是否是解决此问题的正确方法?
- 如果(1)或(2)都不是“好的”假设,那么会发生什么?
Come across what looks at first sight like an MT-issue, but I'm trying to understand in detail the STA model used by COM+.
Effectively, I have a legacy COM+ component, written in VB6, that calls into a native (i.e., not-COM) Win32 DLL written in C++.
Having some intermittant (and impossible to reproduce in testing) problems with it, I added some debugging code to find out what was going on and found that when the problems occur, I had log messages interleaved in the file - so it implied that the DLL was being called by two threads at once.
Now the logging goes to a per-thread file based on _getpid() and GetCurrentThreadId(), so it seems that when the code in the C++ DLL is called, it's getting called twice on the same thread at the same time. My understanding of STA says suggests this could be the case as COM marshalls the individual instances of objects onto a single thread suspends and resumes execution at will.
Unfortuantly I'm not sure where to go from here. I'm reading that I should be calling CoInitialiseEx() in DllMain() to tell COM that this is an STA DLL, but other places say this is only valid for COM DLLs and won't have any effect in a native DLL. The only other option is to wrap parts of the DLL up as critical sections to serialize access (taking whatever performance hit that has on the chin).
I could try and rework the DLL, but there is no shared state or global vars - everything's in local variables so in theory each call should get its own stack, but I'm wondering if the STA model is basically having some odd effect on this and just re-entering into the already loaded DLL at the same entry point as another call. Unfortuantly, I don't know how to prove or test this theory.
The questions basically are:
- When an STA COM+ component calls a native DLL, there's nothing in the STA model to
prevent the active "thread" being suspended and control being passed over to another "thread" in the middle of a DLL call? - Is CoInitialiseEx() the right way to resolve this, or not?
- If neither (1) or (2) are "good" assumptions, what's going on?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(2)
在单元线程 COM 服务器中,保证 COM 类的每个实例都可以由单个线程访问。 这意味着实例是线程安全的。 但是,可以使用不同的线程同时创建许多实例。 现在,就 COM 服务器而言,您的本机 DLL 不必执行任何特殊操作。 想想 kernel32.dll,每个可执行文件都会使用它 - 当 COM 服务器使用它时,它会初始化 COM 吗?
从 DLL 的角度来看,您必须确保线程安全,因为不同的实例可以同时调用您。 在这种情况下,STA 不会保护您。 既然你说你没有使用任何全局变量,我只能假设问题出在其他地方,并且恰好在似乎指向 COM 内容的情况下出现。 您确定没有一些普通的旧 C++ 内存问题吗?
In an apartment threaded COM server, each instance of the COM class is guaranteed to be accesses by a single thread. This means the instance is thread safe. However, many instances can be created simultaniously, using different threads. Now, as far as the COM server is concerned, your native DLL does not have to do anything special. Just think about kernel32.dll, which is used by every executable - does it initialize COM when used by a COM server?
From the DLL point of view, you have to make sure you're thread safe, as different instances can call you at the same time. STA will not protect you on this case. Since you say you are not using any global variables, I can only assume the problem is elsewhere, and just happens to show on circumstances that seem to point to the COM stuff. Are you sure you don't have some plain old C++ memory issues?
我怀疑你的问题是在被调用的 DLL 深处的某个地方,它对另一个单元(同一进程中的另一个线程,或者 MTA 中的一个对象,或者完全是另一个进程)进行了出站 COM 调用。 COM 允许等待出站调用结果的 STA 线程接收另一个入站调用,并递归地处理它。 它仅适用于相同对象之间正在进行的对话 - 即 A 调用 B,B 回调 A,A 再次回调 B - 但如果您向多个客户端或客户端分发了接口指针,则可以接收来自其他对象的调用已将接口指针共享给另一个客户端。 通常,将指向单线程对象的接口指针分发给多个客户端线程是一个坏主意,因为它们只需要互相等待。 每个线程创建一个工作对象。
COM 无法在任何线程上随意挂起和恢复执行 - STA 线程上的新传入调用只能通过消息泵到达。 当“阻塞”等待响应时,STA 线程实际上正在泵送消息,并使用消息过滤器(请参阅 IMessageFilter)检查是否应处理该消息。 但是,消息处理程序不得发出新的传出呼叫 - 如果这样做,COM 将返回 RPC_E_CANTCALLOUT_INEXTERNALCALL 错误(“在消息过滤器内呼出是非法的。”)
如果您有消息泵(GetMessage/DispatchMessage),则可能会出现类似的问题) 本机 DLL 中的任何位置。 我在界面过程中遇到了 VB 的 DoEvents 问题。
CoInitializeEx 只能由线程的创建者调用,因为只有他们知道他们的消息泵送行为是什么。 如果您尝试在 DllMain 中调用它,它很可能会失败,因为您的本机 DLL 正在被调用以响应 COM 调用,因此调用者最终必须已经在线程上调用了 CoInitializeEx 才能进行调用。 对于新创建的线程,在 DLL_THREAD_ATTACH 通知中执行此操作可能会在表面上起作用,但如果 COM 在应该泵送时发生阻塞,则会导致程序出现故障,反之亦然。
I suspect your problem was that somewhere deep within the called DLL, it made an outbound COM call to another apartment (another thread in the same process, or an object in the MTA, or another process entirely). COM permits an STA thread waiting for the result of an outbound call to receive another inbound call, processing it recursively. It's intended only for ongoing conversations between the same objects - i.e. A calls B, B calls A back, A calls B back again - but can receive calls from other objects if you've handed out an interface pointer to multiple clients, or the client has shared the interface pointer to another client. Generally it's a bad idea to hand out interface pointers to a single-threaded object to multiple client threads, as they'll only have to wait for each other. Create one worker object per thread.
COM cannot suspend and resume execution at will on any thread - a new incoming call on an STA thread can only arrive through the message pump. When 'blocked' waiting for a response, the STA thread is actually pumping messages, checking with the Message Filter (see IMessageFilter) whether the message should be handled. However, message handlers must not make a new outgoing call - if they do COM will return an RPC_E_CANTCALLOUT_INEXTERNALCALL error ("It is illegal to call out while inside message filter.")
Similar problems could occur if you have a message pump (GetMessage/DispatchMessage) anywhere within the native DLL. I've had problems with VB's DoEvents in interface procedures.
CoInitializeEx should only be called by the creator of a thread, because only they know what their message pumping behaviour will be. It's likely that if you try to call it in DllMain it will simply fail, as your native DLL is being called in response to a COM call, so the caller must have ultimately already called CoInitializeEx on the thread in order to make the call. Doing it in the DLL_THREAD_ATTACH notification, for newly-created threads, might work superficially but cause the program to malfunction if COM blocks when it should pump and vice-versa.