登录 用户中心() [退出] 后台管理 注册
   
您的位置: 首页 >> 程序员学前班[不再更新,只读] >> 主题: 理解 COM 套间 [转]     [回主站]     [分站链接]
标题
理解 COM 套间 [转]
clq
2007-9-6 13:40:56 发表 编辑


[图片]



当打印出一个线程ID的时候,程序就停止了。Why?刚开始,我也被搞的头晕脑胀。到MSDN中查找WaitForSingleObject,原来WaitForSingleObject会破坏程序中的消息机制,这样在创建的线程中,TestFunc1需要通过消息机制来运行,消息机制破坏,就无法运行了。哎!还的再改程序。在查查《Win32多线程程序设计》,原来在GUI中等待线程需要用MsgWaitForMultipleObjects。好的,我们需要重新写一个函数,专门用来实现消息同步。
DWORD ApartMentMsgWaitForMultipleObject(HANDLE *hHandle,DWORD dwWaitCout, DWORD dwMilliseconds)
{
BOOL bQuit = FALSE;
DWORD dwRet;
while(!bQuit)
{
int rc;
rc = ::MsgWaitForMultipleObjects
  (
dwWaitCout, // 需要等待的对象数量
hHandle, // 对象树组
FALSE, //等待所有的对象
(DWORD)dwMilliseconds, // 等待的时间
(DWORD)(QS_ALLINPUT | QS_ALLPOSTMESSAGE)  // 事件类型  
  );
//等待的事件激发
if( rc == WAIT_OBJECT_0 )
{
dwRet = rc;
bQuit = TRUE;
}
//其他windows消息
else if( rc == WAIT_OBJECT_0 + dwWaitCout )
{
MSG msg;
while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
{
TranslateMessage (&msg);
DispatchMessage(&msg);
}
}
}
return dwRet;
}
  该函数用来处理消息的同步,也够麻烦的,还需要自己写这段程序。这段程序的意思是如果等待的事件被激发,那么设置bQuit为TURE,那么退出消息循环。如果接收到其它的消息的话,再分发出去。好了,把我们的程序再改一下:
// ::WaitForSingleObject(hThreads,INFINITE); //等待线程结束
ApartMentMsgWaitForMultipleObject(hThreads,1,INFINITE);

clq
2007-9-6 13:41:14 发表 编辑

[图片]


我们再来看一下运行结果。

clq
2007-9-6 13:44:36 发表 编辑

[图片]


我们可以看到两处调用TestFunc1,得到的线程ID是相同的。我们再通过VC的调试功能来看看第二个TestFunc1的运行过程。我们在两个TesfFunc1调用处设置断点,然后通过F11跟踪进TestFunc1来看看它的调用过程。以下是在Main中的调用过程。

clq
2007-9-6 13:45:20 发表 编辑

[图片]



通过Call Stack,我们可以看到,此处是在main中直接调用的。我们再来看第二处调用:

clq
2007-9-6 13:45:37 发表 编辑


我们可以看到TestFunc1的调用需要通过一连串的API方法来实现。你感兴趣的话,可以通过反汇编的方法来跟踪一下这些API,看看它们具体实现了什么,这里我们可以看到这些函数在dll中的大致位置,你可以使用W32DASM等反汇编工具打开这些dll,大致研究一下这些函数。
  好了,我们已经看到了Single套间的作用。那么Single套间究竟是什么意思呢?就是说每个被标志为Single的接口,在一个进程中只会存活在一个套间中。该套间就是进程创建的第一个套间。你可以将Main中与pTest相关的代码都去掉,只保留CoInitializeEx(NULL, COINIT_APARTMENTTHREADED)和线程的创建,再次运行该程序,可以发现创建线程中的TestFunc1仍然是通过消息来实现的。
  好了看过了Single,我们还是在注册表中,将ThreadingModel改为Apartment。通过修改注册表就可以实现对套间类型的控制,证明了套间和我们的程序本身没有什么关系,ATL的选项所做的作用也只是通过它来添加注册表。套间只是对系统的一种提示,由COM API通过注册表信息来帮我们实现套间。

  2、Apartment

  在第二部分(套间所要解决的问题),我们曾经提供了一个不同线程共享接口对象的方法,该方法是错误的(我们也可以通过程序阻止这种用法,稍候再叙)。此处我们提供一种正确的做法。以下代码在Apartment/Apartmenttest下可以找到。

#define _WIN32_WINNT 0x0400
#include
#include
#include "..\TestComObject1\TestComObject1_i.c"
#include "..\TestComObject1\TestComObject1.h"
DWORD WINAPI ThreadProc(LPVOID lpv)
{
//HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
HRESULT hr = CoInitializeEx(NULL, COINIT_MULTITHREADED);
if ( FAILED(hr) )
{
std::cout << "CoinitializeEx failed!" << std::endl;
return 0;
}
IStream *pStream = (IStream*)lpv;
ITestInterface1 *pTest = NULL;
hr = ::CoGetInterfaceAndReleaseStream(pStream,
     IID_ITestInterface1,
     (void**)&pTest);
if ( FAILED(hr) )
{
std::cout << "CoGetInterfaceAndReleaseStream failed!" << std::endl;
return 0;
}
hr = pTest->TestFunc1();
if ( FAILED(hr) )
{
std::cout << "TestFunc1 failed!" << std::endl;
return 0;
}
pTest->Release();
::CoUninitialize();
return 0;
}
DWORD ApartMentMsgWaitForMultipleObject(HANDLE *hHandle,DWORD dwWaitCout, DWORD dwMilliseconds)
{
BOOL bQuit = FALSE;
DWORD dwRet;
while(!bQuit)
{
int rc;
rc = ::MsgWaitForMultipleObjects
(
dwWaitCout,    // 需要等待的对象数量
hHandle, // 对象树组
FALSE, //等待所有的对象
(DWORD)dwMilliseconds, // 等待的时间
(DWORD)(QS_ALLINPUT | QS_ALLPOSTMESSAGE)  // 事件类型  
);
if( rc == WAIT_OBJECT_0 )
{
dwRet = rc;
bQuit = TRUE;
}
else if( rc == WAIT_OBJECT_0 + dwWaitCout )
{
MSG msg;
while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
{
  TranslateMessage (&msg);
  DispatchMessage(&msg);
}
}
}
return dwRet;
}
int main(int argc, char* argv[])
{
//HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
HRESULT hr = CoInitializeEx(NULL, COINIT_MULTITHREADED);
if ( FAILED(hr) )
{
std::cout << "CoinitializeEx failed!" << std::endl;
return 0;
}
ITestInterface1 *pTest = NULL;
hr = ::CoCreateInstance(CLSID_TestInterface1,
   0,
   CLSCTX_INPROC,
   IID_ITestInterface1,
   (void**)&pTest);
if ( FAILED(hr) )
{
std::cout << "CoCreateInstance failed!" << std::endl;
return 0;
}
hr = pTest->TestFunc1();
if ( FAILED(hr) )
{
std::cout << "TestFunc1 failed!" << std::endl;
return 0;
}
IStream *pStream = NULL;
hr = ::CoMarshalInterThreadInterfaceInStream(IID_ITestInterface1,
     pTest,
     &pStream);
if ( FAILED(hr) )
{
std::cout << "CoMarshalInterThreadInterfaceInStream failed!" << std::endl;
return 0;
}
DWORD threadID;
HANDLE hThreads[1];
hThreads[0] = ::CreateThread(NULL, //创建一个进程
    0,
    ThreadProc,
    (LPVOID)pStream, //将pStream作为一个参数传入新线程
    0,
    &threadID);
ApartMentMsgWaitForMultipleObject(hThreads,1,INFINITE);
::CloseHandle(hThreads); //关闭线程句柄
pTest->Release();
::CoUninitialize();
system("pause");
return 0;
}

我们通过CoGetInterfaceAndReleaseStream将main中的pTest变为pStream,然后将pStream作为参数传入到线程中,然后再通过CoGetInterfaceAndReleaseStream将pSteam变为接口指针。再来看看运行的结果:

clq
2007-9-6 13:52:10 发表 编辑

[图片]



 可以看到两次运行,线程ID是相同的。好的,我们接着改变注册表,再将Apartment变为Free。然后再将两处的HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);改为HRESULT hr = CoInitializeEx(NULL, COINIT_MULTITHREADED)。编译后再次执行该程序,再来看执行结果。

clq
2007-9-6 13:52:40 发表 编辑

[图片]



我们可以看到两个线程的ID是不同的。你可以通过VC的Debug来看这两组程序的TesFunc1的调用情况,在第二种情况下,创建的线程中不会通过消息机制来调用该函数。
通过对比,我们可以知道所说的套间,就是通过消息机制来控制不同线程中对对象的调用。这样就不需要组件的实现者来实现数据的同步。

  3、Free

  上节的例子,已经为我们提示了我们Free套间,其实系统对我们的组件不做控制,这样就需要组件的开发者对数据的同步做出控制。

  4、Both

  所谓Both,就是说该对象既可以运行在Apartment中,也可以运行在Free套间中。该类型的前提是它应该是Free类型的套间,也就是说组件自己实现了数据的同步。然后设置成Both类型。
为什么需要Both类型的套间呢?想想假如我们在我们的组件中调用另一个组件,这样我们就需要在我们的组件中为所调用的组件来开辟一个套间。我们的套间是一个Apartment,而调用的组件是Free类型的,这样这两个对象就必须存在于不同的两个套间中。而跨套间的调用,需要通过中间代理来实现,这样必然会损失性能。但如果我们调用的套间类型是Both的话,它就可以和我们的组件同享一个套间,这样就可以提高效率。

五、缺省套间

  继续我们的测试,首先在注册表中将我们的接口类型改回Apartment。然后新建一个工程DefaultApartment。C++文件中的实现代码如下。
#define _WIN32_WINNT 0x0400
#include
#include
#include "..\TestComObject1\TestComObject1_i.c"
#include "..\TestComObject1\TestComObject1.h"
DWORD WINAPI ThreadProc(LPVOID lpv)
{
HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
//HRESULT hr = CoInitializeEx(NULL, COINIT_MULTITHREADED);
if ( FAILED(hr) )
{
std::cout << "CoinitializeEx failed!" << std::endl;
return 0;
}
IStream *pStream = (IStream*)lpv;
ITestInterface1 *pTest = NULL;
hr = ::CoGetInterfaceAndReleaseStream(pStream,
     IID_ITestInterface1,
     (void**)&pTest);
if ( FAILED(hr) )
{
std::cout << "CoGetInterfaceAndReleaseStream failed!" << std::endl;
return 0;
}
std::cout << "ThradProc''s threadid is " << ::GetCurrentThreadId() << std::endl; //输出ThradProc的线程ID
hr = pTest->TestFunc1();
if ( FAILED(hr) )
{
std::cout << "TestFunc1 failed!" << std::endl;
return 0;
}
pTest->Release();
::CoUninitialize();
return 0;
}
DWORD ApartMentMsgWaitForMultipleObject(HANDLE *hHandle,DWORD dwWaitCout, DWORD dwMilliseconds)
{
BOOL bQuit = FALSE;
DWORD dwRet;
while(!bQuit)
{
int rc;
rc = ::MsgWaitForMultipleObjects
(
dwWaitCout,    // 需要等待的对象数量
hHandle, // 对象树组
FALSE, //等待所有的对象
(DWORD)dwMilliseconds, // 等待的时间
(DWORD)(QS_ALLINPUT | QS_ALLPOSTMESSAGE)  // 事件类型  
);
if( rc == WAIT_OBJECT_0 )
{
dwRet = rc;
bQuit = TRUE;
}
else if( rc == WAIT_OBJECT_0 + dwWaitCout )
{
MSG msg;
while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
{
  TranslateMessage (&msg);
  DispatchMessage(&msg);
}
}
}
return dwRet;
}
int main(int argc, char* argv[])
{
HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
//HRESULT hr = CoInitializeEx(NULL, COINIT_MULTITHREADED);
if ( FAILED(hr) )
{
std::cout << "CoinitializeEx failed!" << std::endl;
return 0;
}
ITestInterface1 *pTest = NULL;
hr = ::CoCreateInstance(CLSID_TestInterface1,
   0,
   CLSCTX_INPROC,
   IID_ITestInterface1,
   (void**)&pTest);
if ( FAILED(hr) )
{
std::cout << "CoCreateInstance failed!" << std::endl;
return 0;
}
std::cout << "main''s threadid is " << ::GetCurrentThreadId() << std::endl; //打印main的线程ID
hr = pTest->TestFunc1();
if ( FAILED(hr) )
{
std::cout << "TestFunc1 failed!" << std::endl;
return 0;
}
IStream *pStream = NULL;
hr = ::CoMarshalInterThreadInterfaceInStream(IID_ITestInterface1,
     pTest,
     &pStream);
if ( FAILED(hr) )
{
std::cout << "CoMarshalInterThreadInterfaceInStream failed!" << std::endl;
return 0;
}
DWORD threadID;
HANDLE hThreads[1];
hThreads[0] = ::CreateThread(NULL, //创建一个进程
    0,
    ThreadProc,
    (LPVOID)pStream, //将pStream作为一个参数传入新线程
    0,
    &threadID);
ApartMentMsgWaitForMultipleObject(hThreads,1,INFINITE);
::CloseHandle(hThreads); //关闭线程句柄
pTest->Release();
::CoUninitialize();
system("pause");
return 0;
}

clq
2007-9-6 13:56:48 发表 编辑

[图片]


此部分代码与我们测试Apartment时的代码基本相同,只是新增了输出main和创建线程的ID的语句。好的,我们来运行程序,可以得到如上的结果:
  

  我们可以看到main的线程ID和两个TestFunc1的线程ID相同。也就是说两个TestFunc1都是在main的线程中运行的。
将我们的程序做些变动,将CoInitializeEx(NULL, COINIT_APARTMENTTHREADED)改为 CoInitializeEx(NULL, COINIT_MULTITHREADED)。然后接着运行程序。我们再来看运行的结果。

clq
2007-9-6 13:57:08 发表 编辑

[图片]


我们可以看到两个TestFunc1的线程ID和main的不同了,和我们创建的线程也不同。这是为什么呢?CoInitializeEx是一个创建套间的过程,我们使用CoInitializeEx(NULL, COINIT_MULTITHREADED)后,没有为我们的组件创建合适的套间。这时候系统(也就是COM API,这里应该是通过CoCreateInstance来实现的)就会帮我们将我们的接口对象放入缺省套间,该套间并不运行在当前的线程中。我们再次在Debug下跟踪运行过程,可以发现在main中调用TestFunc1,也需要通过众多的API函数帮助完成,也就是说此处也是通过消息机制来完成的,这样性能上肯定会有影响。

clq
2007-9-6 13:58:32 发表 编辑



六、阻止接口指针的非法使用

  在第二部分我们给出了一个通过直接传输接口指针到另外线程的例子,事实上这种方法是错误的,但COM API并没有帮助我们阻止这样的错误。这个任务可以由我们自己来完成。
  因为套间是和线程相关的,Apartment类型的接口方法只应该运行在一个套间中(其实这就是一个协议,并不是强制性的),那么我们可以通过线程的相关性质来实现。
  在线程中我们可以通过Thread Local Storage(TLS)来保存线程的相关信息,同一函数运行在不同的线程中,那么它所拥有的TLS也不相同。
  我们来动手改造我们的类实现,将CTestferface1进行改造。
class ATL_NO_VTABLE CTestInterface1 :
public CComObjectRootEx,
public CComCoClass,
public IDispatchImpl
{
private:
DWORD dwTlsIndex;
public:
CTestInterface1()
{
dwTlsIndex = TlsAlloc();
HLOCAL l = LocalAlloc(LMEM_FIXED, 1);
TlsSetValue(dwTlsIndex, l);  
}
  我们先声明一个私有成员变量dwTlsIndex,它用来存放TLS的索引值(一个线程的TLS相当于一个数组,可以存放不同的数据)。再将构造函数中填入保存数据的代码。此处只是简单的分配了一个字节的地址,并将该地址通过TlsSetValue保存到TLS中去。
  然后再改造我们的TestFunc1函数。如下:
STDMETHODIMP CTestInterface1::TestFunc1()
{
// TODO: Add your implementation code here
LPVOID lpvData = TlsGetValue(dwTlsIndex);
if ( lpvData == NULL )
return RPC_E_WRONG_THREAD;
std::cout << "In the itestinferface1''s object, the thread''s id is " << ::GetCurrentThreadId() << std::endl;
return S_OK;
}
  这边也很简单,就是简单的通过TlsGetValue去尝试得到dwTlsIndex所标志的内容是否存在。如果不存在,那么就说明程序运行在了不同的套间中。就会返回RPC_E_WRONG_THREAD,这是COM设计者定义的宏,表示线程的非法使用。(由于我的懒惰,不再写新的COM了,只是简单的修改了TestComObject1,这部分新加的代码被我注释掉了,你如果想看这部分的效果,去掉注释就可以了)
  我们再运行ErrorUseApartment程序,发现TestFunc1已经无法输出线程号,而是直接返回RPC_E_WRONG_THREAD。再次运行ApartmentTest程序,发现这样的处理对它并没有影响。仍然正常运行。


六、什么是套间?

  我们从外部表现上对套间进行了了解,而套间究竟是什么?潘爱民译的《Com 本质论》说:套间既不是进程,也不是线程,然而套间拥有进程和线程的某些特性。我觉得,这句话翻译的不到位,总让人感觉套间似乎是和进程或者线程等同的东西。找来原文看看:An apartment is neither a process nor a thread; however, apartments share some of the properties of both。这里的share被译成了拥有,但我感觉此处翻译为使用或者分享可能更贴切一些。不过原文事实上也很容易给初学者带来误导。其实套间只是保存在线程中的一个数据结构(还有一个隐藏着的窗口),借用该结构使套间和线程之间建立起某种关系,通过该关系,使得COM API通过该信息可以建立不同套间中的调用机制。这部分涉及到列集,散集(我们调用CoMarshalInterThreadInterfaceInStream,CoGetInterfaceAndReleaseStream的过程)。在列集和散集过程中,COM API会帮我们建立一个不同套间中对象通信机制,这部分涉及到了代理,存根和通道的内容。通过代理来发送调用信息,通过通道发送到存根,再通过存根调用实际的方法(其实那个隐藏的窗口就是为存根来服务的)。所做的这一切不过是为了实现不同套间中可以通过消息来调用对象。你可以找《Com 本质论》来看看,这部分的内容比较繁杂,但我感觉比起套间的概念,还是比较容易的。
  具体实现套间,在线程的TLS究竟保存了什么信息呢?罪恶的微软隐藏了这边部分内容,我们无法得到这部分的材料。这可能也是套间理解起来如此困难的一个原因,套间呈现给我们的是一个抽象的概念。但理解其实际意义后,抽不抽象已经没什么关系,因为它所隐藏的不过是创建和使用套间时候繁杂的调用其它API函数的过程,事实上并没有太多的神秘可言。对我们开发者来说,能明白套间的意义,已经足够了。
  好了,稍微总结一下:套间是保存在线程的TLS中的一个数据结构,通过该结构可以帮助不同的套间之间通过消息机制来实现函数的调用,以保证多线程环境下,数据的同步。


结语

  石康说:比尔.盖茨并不是什么天才,软件工作者充其量不过是一个技术工作者,无法和科学工作者同日而语。石康还说:如果给他老人家足够的时间,他也可以写出一个操作系统。呵呵,大意好象如此,似乎是他老人家在《支离破碎》中的名言,现在记不太清楚了。刚开始觉得他老人家太狂了,不过仔细体会一下,确实如此。计算机的世界很少有真正高深的东西,有些内容你不理解,肯定是你的某方面的基础不扎实。不理解接口,那是因为你的C++没学好;不理解套间,那是因为你不懂多线程;不懂多线程那是因为你不懂CPU的结构。
  技术革新在眼花缭乱的进行的,.Net,Web services,到处闪现着新鲜的名词,似乎这个世界每天都在变化的。但事实上,从286到386,从dos到图形操作系统后,计算机再没有什么重大的革新。从我们开发者的角度来看,不过是开发工具的更新。但每次开发工具的更新都能使很多人兴奋异常,激动着下载安装最新版本的工具,追逐着学习最新的开发语言。总觉的这样就不会被时代所抛弃,总以为开发工具会帮着提升自己的价值。事实上呢?学会拖拉创建窗口的人,可能根本不知道Windows中有一个消息机制。开发十多年的人会把一个栈中生成的对象的地址作为参数传给接收者。没有学会走的时候,不要去跑。我自己也在迷茫中探索着自己的路,现在有点明白老子所说的“企者不立,跨者不行”。
  好了,废话就此打住吧!只是想告诉你,其实编程并没有那么困难,如果有什么东西没明白,别着急,找基础的东西去看。学好COM也一样,看不懂的话,先把C++中的虚函数学明白,再去了解一下多线程的内容。其实也没那么复杂!

有人说,COM过时了,我也不清楚COM的将来会怎么样,但我觉得理解一个东西总是有乐趣的。与你同勉。


总数:20 页次:2/2 首页 << 上一页 尾页  
总数:20 页次:2/2 首页 << 上一页 尾页  


所在合集/目录



发表评论:
文本/html模式切换 插入图片 文本/html模式切换


附件:



NEWBT官方QQ群1: 276678893
可求档连环画,漫画;询问文本处理大师等软件使用技巧;求档softhub软件下载及使用技巧.
但不可"开车",严禁国家敏感话题,不可求档涉及版权的文档软件.
验证问题说明申请入群原因即可.

Copyright © 2005-2020 clq, All Rights Reserved
版权所有
桂ICP备15002303号-1