您的位置:首页 > 其它

COM线程模型 - STA接口 (跨线程传递对象,消息循环)

2014-08-25 23:48 639 查看
前面一篇文章讲述了STA客户调用STA对象和MTA客户调用STA对象,其实并不难理解。

现在就来讲一下如何把一个COM对象传递到另外一个线程。

先来看看STA套间里面创建STA对象,并且传递到另外一个线程的情况。

STA客户创建STA对象,然后传递到另外一个线程

我们先改一下代码,这段代码很简单,只是修改上个文章里面的客户代码。其实就是主线程创建一个COM对象,然后传递到线程里面,起5个线程。

// TestCom.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"

#include <atlbase.h>
#include <thread>
#include <vector>

#include "../MyCom/MyCom_i.h"
#include "../MyCom/MyCom_i.c"

void Test(CComPtr<ICircle>& spCircle)
{
WCHAR temp[100] = { 0 };
swprintf_s(temp, L"STA calling thread (used passed in com object): %d\n", ::GetCurrentThreadId());
OutputDebugStringW(temp);

CoInitialize(NULL);

spCircle->Draw(CComBSTR(L"yellow"));

CoUninitialize();
}

int _tmain(int argc, _TCHAR* argv[])
{
CoInitialize(NULL);
WCHAR temp[100] = { 0 };
swprintf_s(temp, L"Main thread: %d\n", ::GetCurrentThreadId());
OutputDebugStringW(temp);

{
CComPtr<ICircle> spCircle;
spCircle.CoCreateInstance(CLSID_Circle, NULL, CLSCTX_INPROC);

spCircle->Draw(CComBSTR(L"red"));

std::vector<std::thread> vThreads;
for (int i = 0; i < 5; i++)
{
vThreads.push_back(std::thread(Test, spCircle));
}

for (auto& t: vThreads)
{
t.join();
}
}

CoUninitialize();

return 0;
}

运行了一下,得到:



好像每一次COM调用都是在客户线程里面。其实我也不知道为什么会这样,但是我觉得这是不对的,这样做的后果是不可预测的。而且根据STA的定义,STA对象应该是串行化的执行同一个对象的方法。为此,我又特地在COM函数里面sleep一下,如:

STDMETHODIMP CCircle::Draw(BSTR color)
{
// TODO: Add your implementation code here
WCHAR temp[100] = { 0 };
swprintf_s(temp, L"ICircle::Draw, color: %s, tid: %d\n", color, ::GetCurrentThreadId());
OutputDebugStringW(temp);

std::this_thread::sleep_for(std::chrono::milliseconds(100));

OutputDebugStringW(L"ICircle::Draw, end\n");
return S_OK;
}

运行结果是:



好像是并发的,根本就不是串行的,这个根本不符合STA的定义啊。为什么?

其实我们这么调,根本就是错的。如果回顾一下前面的文章,里面有几条规则

1. STA对象只能存在于一个线程中;

2. 如果要跨线程传递COM对象的话,一定要做Marshal

3. 一个STA对象只接受来自创建它的STA线程的调用。

等等。

上面的做法在主线程创建了一个STA对象,然后直接把它传递了其他线程,这根本不符合STA接口的规则,不知道会发生什么事,而且调用也不是线性的。

那么正确的做法应该是怎么样的呢?

Marshal和Unmarshal

通常中文翻译成列集和散集,我还是习惯叫做Marshal和unmarshal。

好,我们现在不直接com指针给线程了,我们先marshal,然后在线程函数里面unmarsha,如:

// TestCom.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"

#include <atlbase.h>
#include <thread>
#include <vector>

#include "../MyCom/MyCom_i.h"
#include "../MyCom/MyCom_i.c"

void Test(LPSTREAM pStream)
{
WCHAR temp[100] = { 0 };
swprintf_s(temp, L"STA calling thread (used passed in com object): %d\n", ::GetCurrentThreadId());
OutputDebugStringW(temp);

CoInitialize(NULL);

CComPtr<ICircle> spCircle;
HRESULT hr = CoGetInterfaceAndReleaseStream(pStream, IID_ICircle, (LPVOID*)&spCircle);  // unmarshal to get a com object
if (SUCCEEDED(hr))
{
spCircle->Draw(CComBSTR(L"yellow"));
}

CoUninitialize();
}

int _tmain(int argc, _TCHAR* argv[])
{
CoInitialize(NULL);
WCHAR temp[100] = { 0 };
swprintf_s(temp, L"Main thread: %d\n", ::GetCurrentThreadId());
OutputDebugStringW(temp);

{
CComPtr<ICircle> spCircle;
spCircle.CoCreateInstance(CLSID_Circle, NULL, CLSCTX_INPROC);

spCircle->Draw(CComBSTR(L"red"));

std::vector<std::thread> vThreads;
for (int i = 0; i < 5; i++)
{
LPSTREAM pStream = nullptr;
CoMarshalInterThreadInterfaceInStream(IID_ICircle, spCircle, &pStream);  // marshal

vThreads.push_back(std::thread(Test, pStream));  // pass a stream instead of com object
}

for (auto& t: vThreads)
{
t.join();
}
}

CoUninitialize();

return 0;
}


就这么运行下,完蛋了,除了主线程的那个Draw调用成功,其他辅助线程里面的调用,Draw函数都出不来了。这又是为什么?

现在就该消息循环出场了。之前有讲过,如果COM对象不需要传递到其他线程的话,那么其实不需要消息循环,但是如果需要传递到其他线程的话,就一定要创建一个消息循环。

消息循环

这里讲的消息循环,其实就是Windows的消息循环。当其他线程调用COM对象的函数的时候(通过列集,散列),其实COM系统就是往创建COM对象的线程发送windows消息。因有关消息循环可以参考这篇文章里面的描述,相对到位。http://www.codeproject.com/Articles/9190/Understanding-The-COM-Single-Threaded-Apartment-Pa

我这里截取了一部分,但是我觉得大家有兴趣应该仔细阅读。



反正,STA对象的线性调用其实就是通过Windows的消息循环来实现的。

 

ok,现在我们来尝试给我们的主调STA客户加上消息循环。代码也很简单,直接贴:

// TestCom.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"

#include <atlbase.h>
#include <thread>
#include <vector>
#include <windows.h>

#include "../MyCom/MyCom_i.h"
#include "../MyCom/MyCom_i.c"

LRESULT CALLBACK WndProc_Notify(HWND hWnd, UINT wMsg, WPARAM wParam, LPARAM lParam)
{

return DefWindowProc(hWnd, wMsg, wParam, lParam);
}

void CreateWnd(void)
{

WNDCLASS wc = { 0 };
wc.style = 0;
wc.lpfnWndProc = WndProc_Notify;
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
//    wc.hInstance = g_hInstance;
wc.hIcon = NULL;
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
wc.hbrBackground = (HBRUSH)GetSysColorBrush(COLOR_WINDOW);
wc.lpszMenuName = NULL;
wc.lpszClassName = TEXT("NOTIFY_MSG_LOOP");

RegisterClass(&wc);

HWND g_hNotifyMsgLoop = CreateWindowExW(0,
wc.lpszClassName,
wc.lpszClassName,
WS_OVERLAPPEDWINDOW,
0,
0,
200,
200,
NULL,
NULL,
NULL,
0);

//	ShowWindow(g_hNotifyMsgLoop, SW_HIDE);
}

void Test(LPSTREAM pStream)
{
CreateWnd();

WCHAR temp[100] = { 0 };
swprintf_s(temp, L"STA calling thread (used passed in com object): %d\n", ::GetCurrentThreadId());
OutputDebugStringW(temp);

CoInitialize(NULL);

CComPtr<ICircle> spCircle;
HRESULT hr = CoGetInterfaceAndReleaseStream(pStream, IID_ICircle, (LPVOID*)&spCircle);  // unmarshal to get a com object
if (SUCCEEDED(hr))
{
spCircle->Draw(CComBSTR(L"yellow"));
}

CoUninitialize();
}

int _tmain(int argc, _TCHAR* argv[])
{
CoInitialize(NULL);
WCHAR temp[100] = { 0 };
swprintf_s(temp, L"Main thread: %d\n", ::GetCurrentThreadId());
OutputDebugStringW(temp);

{
CComPtr<ICircle> spCircle;
spCircle.CoCreateInstance(CLSID_Circle, NULL, CLSCTX_INPROC);

spCircle->Draw(CComBSTR(L"red"));

std::vector<std::thread> vThreads;
for (int i = 0; i < 5; i++)
{
LPSTREAM pStream = nullptr;
CoMarshalInterThreadInterfaceInStream(IID_ICircle, spCircle, &pStream);  // marshal

vThreads.push_back(std::thread(Test, pStream));  // pass a stream instead of com object
}

MSG msg;
while (GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}

for (auto& t: vThreads)
{
t.join();
}
}

CoUninitialize();

return 0;
}

就这样,给创建STA对象的线程增加了一个消息循环。现在运行一下,发现:



这次,我们看到每个Draw函数都是串行运行的,draw函数里面的sleep()前后的2个log没有被打乱。而且运行线程都是1376,也就是创建STA对象的线程。这才符合STA要求。

现在我们知道了,如果想要把一个STA对象往另外一个线程传递,就需要:

1. 列集/散列, (marshal/unmarshal)

2. 创建STA对象的线程一定要有个windows消息循环。

至于第二点,看一下msdn上的描述把。反正大概的意思就是,COM系统发现有其他线程(套间)调用COM对象的方法,COM系统就会往创建COM对象的线程发送消息,但是如果那个线程没有消息循环来接收,那就永远都处理不了了。

处理消息循环的时候还有很多细节问题,这个可以查看其他资料。反正真正需要用到消息循环的时候再研究一下。

这里还有另外一种情况,就是MTA客户调用STA对象,然后又想把STA对象传递到其他线程,这又是什么情况呢?下次再研究了...

 

测试代码:http://download.csdn.net/detail/zj510/7818731

 
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: