有段在 Windows XP 和 Vista 上工作了四五年都没有问题的代码在 Windows 7 上出了问题。还好问题定位并不困难:一个 COM 接口的方法返回错误值 ——『调用了为另一个线程 marshall 的接口』。获取这个接口的线程用了『Apartment Threaded』模式。这个模式不允许多个线程直接共享一个 COM 接口的指针,线程间传递 COM 接口必须在一个线程中用 CoMarshalInterThreadInterfaceInStream()
把要传递的 COM 指针转换成 IStream
的形式,再在另一个线程里用 CoGetInterfaceAndReleaseStream()
转换回来。在 Apartment Thread 模式中得到的 COM 指针不是直接指向 COM 组件的裸指针,而是一个 proxy 的指针。一个 proxy 对象只能在一个线程中使用,上面的那对 API 就是用 IStream
作为中间载体在不同线程间创建新的 proxy 对象。正是因为这个 proxy 对象的存在,Apartment Thread 模式能够把多线程对一个 COM 组件的并行调用序列化(Serializing),这样才能让没有考虑多线程的 COM 组件能在多线程环境中被使用。出问题的那段代码就是没有遵循这个约定直接传递了在一个线程里创建的 proxy(最初的开发者可能根本不知道 Apartment Thread 里 proxy 的存在,把这个 proxy 当成了可以直接指向 COM 的指针)。
从 COM 出现的时间来说,Apartment Thread 也算是一种承前启后的设计。让设计时没有考虑线程安全的 COM 组件能够不加修改的在多线程环境中使用。尽管现在看来当时(现在似乎也并无改进) Microsoft 介绍 Apartment Thread 的 文档实在够烂,98 年我怎么也读不懂,三年以后 Sun 的 EJB 文档才让我理解了这种设计。
为了能并发访问线程不安全的 COM 组件,引入一个做为中间层的 proxy 是必要的。但是 Microsoft 的设计者欠考虑的是完全没有必要为每个线程引入一个 proxy。即使每个线程必须有一个 proxy,那么也应该用另一个中间层来隐藏这些 proxy,让第三方开发者只看到一个 proxy。在必须引入中间层的一个地方,最多只应该有一个外部可见的中间层。
COM 在这个问题上的历史疏忽不应该留到现在,今天 Microsoft 完全可以用一种不破坏向后兼容的方式来解决这个问题:放松 Apartment Thread 模式下对 proxy 的约束,通过重新实现 proxy 让同一个 proxy 对象可以在多个线程里直接传递和使用。同时把 CoMarshalInterThreadInterfaceInStream()
和 CoGetInterfaceAndReleaseStream()
变成类似 no-op 的操作。这样,无论是不明就里直接传递了指针的代码还是规规矩矩 marshall proxy 的代码都能正常工作。遗憾的是,Windows 7 不但没有让这样的变化名正言顺,反而把 Vista 和之前版本里本来在实现上放松的约定收紧了。这样的做法从严格的文档意义上说不算错误,但是在对待开发者的态度上,是一种缺乏细心的考虑,是 API 的退化。
发表评论