COM 初始化和清理是否适合函数级粒度?
COM initialization and cleanup appropriate at the function-level granularity?
考虑编写一个可重用的自定义函数,在其函数体内创建 COM 对象并调用某些 COM 接口的方法。为了使其正常工作,必须调用 CoInitializeEx
和匹配的 CoUninitialize
API。
在函数体内调用那些 COM 初始化和清理 API 会向调用者隐藏 COM 实现细节,同时也会减轻调用者的负担。
但是在函数体内调用 CoInitializeEx
和匹配的 CoUninitialize
被认为是一种好的编码习惯吗?
在函数粒度级别调用那些 COM init/cleanup 函数是否意味着每个函数调用的开销太大?
这种设计还有其他缺点吗?
这是一种糟糕的做法,而且从根本上是错误的。重要的是第二个参数 (dwCoInit) 的值。它必须是 COINIT_APARTMENTTHREADED,通常缩写为 STA,或 COINIT_MULTITHREADED (MTA)。这是你许下的承诺,誓死不渝的风格。如果你违背了诺言,那么程序就会死掉。通常是由于死锁、未获得预期事件或性能慢得令人无法接受。
当您 select STA 时,您保证线程运行良好并且可以支持非线程安全的 COM 组件。实现这个承诺需要线程泵出一个消息循环并且从不阻塞。例如,支持 GUI 的线程的常见行为。绝大多数 COM 组件都不是线程安全的。
当您 select MTA 时,您根本不承诺任何支持。该组件现在必须自生自灭以保持自身线程安全。通常通过让 COM 基础结构自己创建一个线程来为组件提供一个安全的家来自动完成。您需要注意的进一步细节是封送接口指针,需要 CoMarshalInterThreadInterfaceInStream() 辅助函数或更方便的 IGlobalInterfaceTable 接口。这确保创建一个代理来处理所需的线程上下文切换。
MTA 听起来很方便,但并非没有后果,一个简单的 属性 getter 调用可能会花费多达 x10000 倍的时间。线程上下文切换带来的开销以及跨堆栈帧复制任何参数和 return 值的需要。封送接口指针很容易失败,COM 组件的作者通常不提供必要的 proxy/stub 或者他们故意省略它,因为复制数据太困难或太昂贵。
关键是图书馆永远无法在 STA 和 MTA 之间做出选择。它不知道关于线程的 beans,它没有创建那个线程。并且不可能知道线程是否泵出消息循环或块。这些代码完全超出了图书馆的范围。否则 COM 基础结构也需要知道这一点的确切原因,它同样无法对线程做出假设。
选择必须由创建和初始化线程的代码做出,通常是应用程序本身。除非库为了使调用安全而创建线程。但是随之而来的结果是代码总是很慢。您通过 return 不可避免的 CO_E_NOTINITIALIZED 错误代码提醒您的库的调用者他没有正确处理。
Fwiw,这是您在 .NET Framework 中看到的东西。 CLR 总是在线程可以执行任何托管代码之前调用 CoInitializeEx()。仍然是应用程序的程序员必须做出的选择,或者更典型的是项目模板,通过 Main() 上的 [STAThread] 属性或工作线程的 Thread.SetApartmentState() 调用来完成。
考虑编写一个可重用的自定义函数,在其函数体内创建 COM 对象并调用某些 COM 接口的方法。为了使其正常工作,必须调用 CoInitializeEx
和匹配的 CoUninitialize
API。
在函数体内调用那些 COM 初始化和清理 API 会向调用者隐藏 COM 实现细节,同时也会减轻调用者的负担。
但是在函数体内调用 CoInitializeEx
和匹配的 CoUninitialize
被认为是一种好的编码习惯吗?
在函数粒度级别调用那些 COM init/cleanup 函数是否意味着每个函数调用的开销太大?
这种设计还有其他缺点吗?
这是一种糟糕的做法,而且从根本上是错误的。重要的是第二个参数 (dwCoInit) 的值。它必须是 COINIT_APARTMENTTHREADED,通常缩写为 STA,或 COINIT_MULTITHREADED (MTA)。这是你许下的承诺,誓死不渝的风格。如果你违背了诺言,那么程序就会死掉。通常是由于死锁、未获得预期事件或性能慢得令人无法接受。
当您 select STA 时,您保证线程运行良好并且可以支持非线程安全的 COM 组件。实现这个承诺需要线程泵出一个消息循环并且从不阻塞。例如,支持 GUI 的线程的常见行为。绝大多数 COM 组件都不是线程安全的。
当您 select MTA 时,您根本不承诺任何支持。该组件现在必须自生自灭以保持自身线程安全。通常通过让 COM 基础结构自己创建一个线程来为组件提供一个安全的家来自动完成。您需要注意的进一步细节是封送接口指针,需要 CoMarshalInterThreadInterfaceInStream() 辅助函数或更方便的 IGlobalInterfaceTable 接口。这确保创建一个代理来处理所需的线程上下文切换。
MTA 听起来很方便,但并非没有后果,一个简单的 属性 getter 调用可能会花费多达 x10000 倍的时间。线程上下文切换带来的开销以及跨堆栈帧复制任何参数和 return 值的需要。封送接口指针很容易失败,COM 组件的作者通常不提供必要的 proxy/stub 或者他们故意省略它,因为复制数据太困难或太昂贵。
关键是图书馆永远无法在 STA 和 MTA 之间做出选择。它不知道关于线程的 beans,它没有创建那个线程。并且不可能知道线程是否泵出消息循环或块。这些代码完全超出了图书馆的范围。否则 COM 基础结构也需要知道这一点的确切原因,它同样无法对线程做出假设。
选择必须由创建和初始化线程的代码做出,通常是应用程序本身。除非库为了使调用安全而创建线程。但是随之而来的结果是代码总是很慢。您通过 return 不可避免的 CO_E_NOTINITIALIZED 错误代码提醒您的库的调用者他没有正确处理。
Fwiw,这是您在 .NET Framework 中看到的东西。 CLR 总是在线程可以执行任何托管代码之前调用 CoInitializeEx()。仍然是应用程序的程序员必须做出的选择,或者更典型的是项目模板,通过 Main() 上的 [STAThread] 属性或工作线程的 Thread.SetApartmentState() 调用来完成。