ATL一节已经接触过多线程一词,COM为了解决多线程问题的思路如下:
1.支持传统多线程访问,组件编写时处理多线程访问(MTA)
2.支持隐藏多线程细节,组件编写时无需多线程访问(STA)
前者是为了灵活,后者是为了方便和兼容,下面我们从这两点展开来说。
1.套间分类
COM引入一个概念——套间(Apartment),具有相同并发和重入访问特性的在同一套间。每个进程可包含多个套间,但是一个套间只能属于一个套间。套间包括两种类型:多线程套间(MTA,multithreaded apartment)和单线程套间(STA,singlethreaded apartment),分别对应前面说的两点。
MTA中多个线程可以并发执行,STA中只有一个线程可以执行,而且这个线程是和这个STA绑定的,STA套间id保存在这个线程的TLS中。
如下,每个进程可有多个STA,但是最多只能有一个MTA。

位于MTA中的对象2可以自由访问,从STA或MTA中另一个线程都可以自由访问。
位于STA中的对象1只能通过,从MTA或另一个STA中访问都必须通过代理,实际上COM实现如下,它是通过一个消息队列来保证每次只有一个方法调用的。

2.套件中的指针和对象
在使用COM时,新线程必须使用如下三个API之一,以便进入套间。
WINOLEAPI CoInitialize(__in_opt LPVOID pvReserved);
WINOLEAPI CoInitializeEx(__in_opt LPVOID pvReserved, __in DWORD dwCoInit);
WINOLEAPI OleInitialize(IN LPVOID pvReserved); 三个函数的第一个参数都是保留参数,传0即可。
CoInitializeEx是最底层API,指明要进入的套间类型,COINIT_MULTITHREADED指明进入MTA,COINIT_APARTMENTTHREADED指明进入STA。其他两个都是默认进入STA。
套间是接口指针的属性——调用API或某个方法返回一个接口指针时,调用该API或方法的线程决定了“结果接口指针将属于哪个套间”。如果COM对象在当前调用线程套间中,那么返回的是对象接口指针,否则是接口代理指针。这里可以看到进程外组件访问其实是跨套间访问的特例。
组件对象指明了自己将运行在何种套间中。
之前在编写ATL COM时可以看到需要需要我们指定当前组件的线程模型,如下:

实际选择对应注册表中ThreadingModel键值,类似如下:

可选择值如下四个:
1.主STA 键值为空,组件只能运行在主STA(进程的第一个STA线程)中运行
2.Apartment 键值为"Apartment",组件只能在STA中执行
3.Free 键值为“Free”,组件只能在MTA中运行
4.Both 键值为“Both”,组件可以在MTA或STA中运行
创建组件对象时,如果当前线程套件模型和组件可运行的套间兼容,那么就会直接返回组件接口指针;否则返回的是组件接口代理。
比如组件指明在STA中运行,我们在MTA中创建组件对象,此时COM会自动新建一个STA,在其中创建对象,返回给我们的是接口代理。
3.跨套间访问
前面说过,进程外组件是跨套间访问的特例。跨套件访问的时候同样需要列集合散集,需要代理/存根。这里使用标准列集演示跨套件访问,如前所说我们采用OLE已有接口IDispatch,这里可以避免自己编写代理存根,否则就需要自己编写代理存根了。
如下我们创建一个Apartment类型的组件,在主STA中创建,然后创建线程,在新的STA中访问组件接口。
如下
{
CComPtr<IMyCircle> spCircle;
HRESULT hr = spCircle.CoCreateInstance(CLSID_MyCircle, NULL, CLSCTX_INPROC);
if (SUCCEEDED(hr))
{
CComBSTR bstrColor(L"红色");
spCircle->Draw(bstrColor);
UseInterfaceInThread3(spCircle);
UseInterfaceInThread2(spCircle);
UseInterfaceInThread(spCircle);
MessageLoop();
}
}
注意这里必须开启线程消息队列,因为之前我们说过STA是通过消息队列来保证STA中方法串行执行的。
执行结果如下,前面数值为当前方法执行的线程id:

这里我们在UseInterfaceInThread3中,直接访问接口指针,可以看到实际方法在调用线程中执行,不能保证串行访问。
在UseInterfaceInThread2我们通过全局表列集散集,在UseInterfaceInThread中我们直接列集散集,可以看到都能保证串行执行。
所以一定要记住,为了跨套间访问的安全性,必须保证列集散集使用。
对应的各个方法代码如下:
//非散集、列集
UINT __stdcall TestThread3(LPVOID lpParam)
{
CoInitialize(NULL);
{
CComPtr<IMyCircle> spCircle = (IMyCircle*)lpParam;
if (spCircle)
{
spCircle->Draw(CComBSTR(L"蓝色"));
}
}
CoUninitialize();
return 0;
}
VOID UseInterfaceInThread3(CComPtr<IMyCircle> &spCircle)
{
HANDLE hThread = (HANDLE)_beginthreadex(NULL, 0, &TestThread3, spCircle, 0, NULL);
if (hThread)
{
CloseHandle(hThread);
}
}
//全局接口表散集、列集
UINT __stdcall TestThread2(LPVOID lpParam)
{
CoInitialize(NULL);
{
if (g_Table)
{
IMyCircle *p = NULL;
HRESULT hr = g_Table->GetInterfaceFromGlobal(g_dwTable, IID_IMyCircle, (LPVOID*)&p);
CComPtr<IMyCircle> spCircle = p;
if (SUCCEEDED(hr) && spCircle)
{
spCircle->Draw(CComBSTR(L"蓝色"));
g_Table->RevokeInterfaceFromGlobal(g_dwTable);//用完之后释放全局表
g_dwTable = 0;
}
}
}
CoUninitialize();
return 0;
}
VOID UseInterfaceInThread2(CComPtr<IMyCircle> &spCircle)
{
CoCreateInstance(CLSID_StdGlobalInterfaceTable, NULL, CLSCTX_INPROC, IID_IGlobalInterfaceTable, (LPVOID*)&g_Table);
if (g_Table && spCircle)
{
g_Table->RegisterInterfaceInGlobal(spCircle, IID_IMyCircle, &g_dwTable);
}
HANDLE hThread = (HANDLE)_beginthreadex(NULL, 0, &TestThread2, spCircle, 0, NULL);
if (hThread)
{
CloseHandle(hThread);
}
}
//散集、列集
UINT __stdcall TestThread(LPVOID lpParam)
{
CoInitialize(NULL);
{
CComPtr<IMyCircle> spCircle;
HRESULT hr = CoGetInterfaceAndReleaseStream((LPSTREAM)lpParam, IID_IMyCircle, (LPVOID*)&spCircle); // unmarshal to get a com object
if (SUCCEEDED(hr))
{
spCircle->Draw(CComBSTR(L"蓝色"));
}
}
CoUninitialize();
Sleep(100000);
PostThreadMessage(g_dwMainThread, WM_QUIT, 0, 0);
return 0;
}
VOID UseInterfaceInThread(CComPtr<IMyCircle> &spCircle)
{
LPSTREAM pStream = NULL;
CoMarshalInterThreadInterfaceInStream(IID_IMyCircle, spCircle, &pStream);
HANDLE hThread = (HANDLE)_beginthreadex(NULL, 0, &TestThread, pStream, 0, NULL);
if (hThread)
{
CloseHandle(hThread);
}
}
如果这里我们去掉MessageLoop呢,可以实际调试运行下,实际上此时UseInterfaceInThread2和UseInterfaceInThread都不会输出了。
完整演示代码下载链接,注意工程中的代理/存根模块本文是没用到的,因为我们使用的是标准列集IDispatch。
原创,转载请注明来自http://blog.csdn.net/wenzhou1219
|