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

前面一篇文章讲述了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接口 (跨线程传递对象,消息循环)_第1张图片
好像每一次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;
}

运行结果是:

COM线程模型 - STA接口 (跨线程传递对象,消息循环)_第2张图片

好像是并发的,根本就不是串行的,这个根本不符合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

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

COM线程模型 - STA接口 (跨线程传递对象,消息循环)_第3张图片

反正,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对象的线程增加了一个消息循环。现在运行一下,发现:

COM线程模型 - STA接口 (跨线程传递对象,消息循环)_第4张图片

这次,我们看到每个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


 

你可能感兴趣的:(COM线程模型 - STA接口 (跨线程传递对象,消息循环))