您所描述的称为重入。
事实上,COM 并没有做任何明确的事情来防止重入问题。由每个对象的实现者在需要时采取预防措施(如果适用)。
有趣的是,COM 中的重入在现实生活中远没有您想象的那么常见。 COM 中的对象图往往主要是树,它们不表现出可重入性。当你有循环时,几乎总是因为对象暴露了某种事件类型的功能,通常是连接点。
事件回调的范围非常有限,它们在每个对象代码的显式控制下触发,因此程序员可以轻松地对它们进行计时,以便它们发生在安全的地方(例如,在方法主体的末尾/附近)所有真正的工作都完成了)。这可以防止出现严重的重入问题。
但是没有什么能阻止你编写危险的代码。例如,如果一个对象在其内部对象状态不一致的情况下触发了一个事件,则所有赌注都关闭。
你提到了死锁。死锁需要某种锁定机制(例如关键部分),并且由于上述原因,在 COM 公寓中应该非常罕见甚至不可能。任何在持有锁的同时触发事件的对象都是在自找麻烦,死锁并不是它最大的担忧:由于是 STA 对象,重入调用将在同一个线程上运行,并且它能够再次获取锁并继续执行,这意味着对象很可能会破坏其内部状态,导致崩溃或更糟。请注意,只有在对象的 STA 之外的线程可以访问由锁控制的资源时,STA 线程中的锁才有意义。
最后,COM 中没有任何东西可以阻止您导致无限递归循环和随后的堆栈溢出。例如,使用两个 COM 对象 Obj1 和 Obj2,其中 Obj2 实现一个事件。我们可以让 Obj1 调用 pObj2->SomeMethod(...) ,这会导致 Obj2 触发事件;然后让 obj1 监听(“sink”)该事件,并让该事件处理程序再次调用 SomeMethod()。
更新:
非常感谢 Remy Lebeau 在他的评论中通过指向 CodeGuru 文章 Understanding COM Apartments, Part I 的链接指出了我忘记讨论的内容。在这个过程中,我自己也学到了一些我应该知道的新东西。
要考虑重入和锁定的一个方面,那就是在单元间调用期间发生的事情(STASTA、STAMTA,甚至 STAOutofProc)。在公寓间呼叫期间,STA(呼叫者的)线程需要停止并等待对呼叫请求的答复;响应不能(根据定义)在同一个线程上执行。但它不能完全阻塞(例如 WaitForSingleObject)等待响应,因为线程需要能够响应和处理不仅对原始对象的潜在回调,而且对任何其他的回调同一公寓内的物体。如果它完全阻塞,COM 基础结构本身就会引入死锁的可能性,您甚至不需要对象之间的依赖循环。因此,COM 编组基础设施使用更复杂的等待形式,它可以在其他一些情况下解除阻塞(Hans Passat 指出 CoWaitForMultipleHandles,这在我看来是正确的,但我不知道该级别的基础设施)。如果发生适用的回调,编组基础设施将解除阻塞并允许该呼叫进入公寓并继续。
这是一种由 COM 基础结构本身引发的锁定形式,而不是作为对象实现的一部分显式编码的形式,这就是我没有想到提出它的原因。所以 COM 确实“做一些事情来防止死锁”,但是是为了防止其自身的基础设施引起的死锁可能性。
我没有意识到的部分是这种机制是非常有选择性的。它只允许构成同一因果链一部分的 COM 调用,即回调,这是线程正在等待的调用的直接结果。进入单元的其他 COM 调用必须排队等待该调用链结束,并等待 STA 线程返回到线程的消息循环。1
1 完全有道理,它需要那样,但我想我从来没有意识到这一点。