COM线程模型 - STA接口

COM里面的线程模型应该是COM里面比较困难的一部分了,我自己也是花了很多时间,而且也还不是很懂。最近又重新看了一下线程模型,为了加深自己的学习,特地写下这篇文章。

MSDN上有很多信息,比如

http://msdn.microsoft.com/en-us/library/windows/desktop/ms680112(v=vs.85).aspx

这里面就有很多关于STA的信息。建议仔细阅读,特别是新手。

STA规则

这里罗列几条重要的规则:

Rules for single-threaded apartments are simple, but it is important to follow them carefully: 
• Every object should live on only one thread (within a single-threaded apartment). 
• Initialize the COM library for each thread. 
• Marshal all pointers to objects when passing them between apartments. 
• Each single-threaded apartment must have a message loop to handle calls from other processes and apartments within the same process. Single-threaded apartments without objects (client only) also need a message loop to dispatch the broadcast messages that some applications use. 
• DLL-based or in-process objects do not call the COM initialization functions; instead, they register their threading model with the ThreadingModel named-value under the InprocServer32 key in the registry. Apartment-aware objects must also write DLL entry points carefully. There are special considerations that apply to threading in-process servers. For more information, see In-Process Server Threading Issues. 

简单翻译一下:

STA的规则很简单,但是需要小心的遵守这些规则: 
• 每一个STA COM 对象只能存在于一个线程中 (在一个STA套间内)
• 每一个线程都需要初始化COM库
• 在套间之间传递com对象指针的时候,需要列集(marshal)
• 每一个STA套间必须拥有一个消息循环,用来处理从其他进程或者当前进程的其他套间过来的消息。(后面一句没有理解,就不翻译了,以免误导。)其实,我个人感觉如果一个STA套间创建了一个COM对象,只要这个COM对象不传递到其他线程,消息循环是可以省略的。但是如果COM对象需要传递到其他进程,那么就必须创建一个消息循环。
• COM对象本身并不需要调用COM的初始化函数;相反,他们会把他们的线程模型放在注册表中的一个叫做InprocServer32的键下面。后面的也不是很了解。以后弄明白了再说。

还是用几个例子来说明吧。

简单的STA COM组件

先来创建一个VS solution,很简单,里面有2个工程,一个是console,一个是ATL工程。看上去就像:

COM线程模型 - STA接口_第1张图片

然后给MyCom增加一个接口,其实就是用ATL向导来做的,很简单,但是还是截一些图吧,这样更加形象。

图1

COM线程模型 - STA接口_第2张图片

图2

COM线程模型 - STA接口_第3张图片

图3

COM线程模型 - STA接口_第4张图片

注意图3中,我们选择了Apartment类型,也就是STA.搜索一下注册表,会发现注册表中有一项来表示COM的线程模型,这个应该就是上面的规则5所说的吧。

COM线程模型 - STA接口_第5张图片


到这里,我们就增加了一个ICircle接口了。这是个空接口,给它增加一个函数吧。还是傻瓜式的用ATL向导。

第一步

COM线程模型 - STA接口_第6张图片

第二步:

COM线程模型 - STA接口_第7张图片

增加了一个很简单的函数,写几行代码吧,很简单,打一个log:

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);


	return S_OK;
}


在测试程序里面,写几行代码,直接贴出来了,因为真的非常简单:

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

#include "stdafx.h"

#include 

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

void Test1()
{
	WCHAR temp[100] = { 0 };
	swprintf_s(temp, L"calling thread: %d\n", ::GetCurrentThreadId());
	OutputDebugStringW(temp);

	CoInitialize(NULL);

	{
		CComPtr spCircle;
		if (SUCCEEDED(spCircle.CoCreateInstance(CLSID_Circle, NULL, CLSCTX_INPROC)))
		{
			spCircle->Draw(CComBSTR(L"red"));
		}
	}


	CoUninitialize();
}

int _tmain(int argc, _TCHAR* argv[])
{
	Test1();

	return 0;
}


运行一下,我们可以得到这个结果:

COM线程模型 - STA接口_第8张图片

OK, 到这里为止,我们就简单写了个COM组件,COM组件里有一个接口叫做ICircle,这个接口里面有个方法叫做Draw。然后在测试程序(console程序)里面调用了一下,得到上面的结果:

1. 调用线程用CoInitialize()初始化

2. 调用线程的号码是2872

3. COM组件的Draw函数里面也打印了线程号,也是2872.

在上面的代码里面,我们可以说是:

1. 我们创建了一个STA的COM组件

2. 我们创建了一个STA的调用环境。

通常我们提交COM的线程模型,其实指的是两方面:一个是客户程序的线程模式,一个是组件所支持的线程模式。客户程序的线程模式只有两种,单线程公寓(STA)和多线程公寓(MTA)。组件所支持的线程模式有四种:Single(单线程)、Apartment(STA)、Free(MTA)、Both(STA+MTA)。

注意,公寓和套间是同一个概念,这只是翻译而已,都是指apartment。

那么我们现在可以说,上面的例子是STA的客户程序调用STA COM组件。

STA客户程序调用STA COM组件

现在来分析一下这种情况的方方面面。首先我要推荐一篇文章,相当nice:http://www.codeproject.com/Articles/9190/Understanding-The-COM-Single-Threaded-Apartment-Pa

建议一个字一个字的读。在这篇文章里面提到了2点:

  1. An STA object created inside an STA thread will reside in the same STA as its thread. 
  2. All objects inside an STA will receive method calls only from the thread of the STA.

很简单,意思是说如果一个STA线程创建了一个STA对象,那么这个对象就存在于当前这个创建它的线程里面。第二点是说一个STA套间里面所有的对象都只接收来自这个套间里面线程的方法调用。

用个图来表示就大概是这个样子:

COM线程模型 - STA接口_第9张图片

一个进程里面可以有多个STA套间,每个STA套间里面可以有多个STA对象。

如果一个客户程序创建了一个STA套间,然后在这个套间里面调用STA对象,这是最简单的一种情况。这种情况下:

1. 客户程序直接调用STA对象;

2. STA COM对象运行在客户程序创建的STA套间的线程里面;

3. 像上面的例子,我们创建的STA COM对象并不会传递到其他线程,那么也就不需要消息循环了。(看上面的代码,根本没有消息循环)

OK, 我们再来看看另外一种情况,MTA客户程序来调用STA COM 对象。

MTA客户程序调用STA COM组件

稍微改一下客户程序,很是简单,就是把刚才的CoInitialize(NULL)改成了CoInitializeEx(NULL, COINIT_MULTITHREADED),代码如下,就是新增了Test2()函数。

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

#include "stdafx.h"

#include 

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

void Test1()
{
	WCHAR temp[100] = { 0 };
	swprintf_s(temp, L"STA calling thread: %d\n", ::GetCurrentThreadId());
	OutputDebugStringW(temp);

	CoInitialize(NULL);

	{
		CComPtr spCircle;
		if (SUCCEEDED(spCircle.CoCreateInstance(CLSID_Circle, NULL, CLSCTX_INPROC)))
		{
			spCircle->Draw(CComBSTR(L"red"));
		}
	}


	CoUninitialize();
}

void Test2()
{
	WCHAR temp[100] = { 0 };
	swprintf_s(temp, L"MTA calling thread: %d\n", ::GetCurrentThreadId());
	OutputDebugStringW(temp);

	CoInitializeEx(NULL, COINIT_MULTITHREADED);

	{
		CComPtr spCircle;
		if (SUCCEEDED(spCircle.CoCreateInstance(CLSID_Circle, NULL, CLSCTX_INPROC)))
		{
			spCircle->Draw(CComBSTR(L"green"));
		}
	}


	CoUninitialize();
}

int _tmain(int argc, _TCHAR* argv[])
{
	Test1();

	Test2();

	return 0;
}

跑一下,看到:

COM线程模型 - STA接口_第10张图片

这次我们看到调用线程和COM运行线程不是同一个,这是为什么?

刚才推荐的文章里面讲的很清楚:All STA objects in a process which are created inside non-STA threads will reside in the default STA.

Default STA:秘密就在这里了,当客户程序创建一个STA COM对象的时候,系统发现当前套间并不是STA(因为我们使用了CoInitializeEx(NULL, COINIT_MULTITHREADED);来创建的,这是MTA套间。这样,系统就会创建一个Default STA来运行STA对象。这个过程无需程序员关心。

这段话很重要:Developers new to the world of COM Apartments please note well this intriguing phenomenon: that even though a call toCreateInstance() orCoCreateInstance() is made inside a thread, the resulting object can actually be instantiated in another thread. This is performed transparently by COM behind the scenes. Please therefore take note of this kind of subtle maneuvering by COM especially during debugging.

大致意思是说:

刚刚接触COM套间的程序员需要注意这一点:在一个线程里面调用CreateInstance或者CoCreateInstance()创建了一个对象,但是有可能这个对象是在另外一个线程里面被实例化。这个过程是COM系统做的。请在debug的时候注意这一点。

其实这个过程可以用下面的图来说明。

COM线程模型 - STA接口_第11张图片

需要重点记住的是:这种情况下,CoCreateInstance返回的并不是真正对象的指针,而是一个代理,proxy。

ok,现在我们知道了为什么COM对象运行线程和客户程序的线程不是同一个。原因很简单,就是因为STA COM对象是在一个default sta里面运行。MTA 客户是不可以直接运行STA COM 对象的。

稍微修改一下代码,再来确定一下这个问题,我们把代码改成如下,其实很简单就是起5个线程来调用Test2().

int _tmain(int argc, _TCHAR* argv[])
{
	Test1();

	std::thread t1(Test2);
	std::thread t2(Test2);
	std::thread t3(Test2);
	std::thread t4(Test2);
	std::thread t5(Test2);

	t1.join();
	t2.join();
	t3.join();
	t4.join();
	t5.join();

	return 0;
}

运行结果:看的很清楚,COM对象是在同一个线程里面运行的。也就是COM系统创建的default STA。

COM线程模型 - STA接口_第12张图片

MTA客户调用STA对象也讲完了。有个问题,那么如果是不同的COM 接口,又如何?事实胜于雄辩,测试一下就好了。新增一个接口IMyRect,同样增加一个函数Draw。

客户端程序改成这样:

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

#include "stdafx.h"

#include 
#include 

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

void Test1()
{
	WCHAR temp[100] = { 0 };
	swprintf_s(temp, L"STA calling thread: %d\n", ::GetCurrentThreadId());
	OutputDebugStringW(temp);

	CoInitialize(NULL);

	{
		CComPtr spCircle;
		if (SUCCEEDED(spCircle.CoCreateInstance(CLSID_Circle, NULL, CLSCTX_INPROC)))
		{
			spCircle->Draw(CComBSTR(L"red"));
		}
	}


	CoUninitialize();
}

void Test2()
{
	WCHAR temp[100] = { 0 };
	swprintf_s(temp, L"MTA calling thread: %d\n", ::GetCurrentThreadId());
	OutputDebugStringW(temp);

	CoInitializeEx(NULL, COINIT_MULTITHREADED);

	{
		CComPtr spCircle;
		if (SUCCEEDED(spCircle.CoCreateInstance(CLSID_Circle, NULL, CLSCTX_INPROC)))
		{
			spCircle->Draw(CComBSTR(L"green"));
		}
	}


	CoUninitialize();
}

void Test3()
{
	WCHAR temp[100] = { 0 };
	swprintf_s(temp, L"MTA calling thread (rect): %d\n", ::GetCurrentThreadId());
	OutputDebugStringW(temp);

	CoInitializeEx(NULL, COINIT_MULTITHREADED);

	{
		CComPtr spRect;
		if (SUCCEEDED(spRect.CoCreateInstance(CLSID_MyRect, NULL, CLSCTX_INPROC)))
		{
			spRect->Draw(CComBSTR(L"blue"));
		}
	}


	CoUninitialize();
}
int _tmain(int argc, _TCHAR* argv[])
{
	Test1();

	std::thread t1(Test2);
	std::thread t2(Test2);
	std::thread t3(Test2);
	std::thread t4(Test3);
	std::thread t5(Test3);

	t1.join();
	t2.join();
	t3.join();
	t4.join();
	t5.join();

	return 0;
}

结果如下:

COM线程模型 - STA接口_第13张图片

 实际上,刚才推荐的文章里面有一句话:All STA objects in a process which are created inside non-STA threads will reside in the default STA.


总结

STA客户调用STA COM组件

1. STA对象在客户创建的STA套间线程里面运行;

2. STA客户直接调用STA COM对象指针;

MTA客户调用STA COM组件

1. STA对象在default STA里面运行,如果有多个STA对象,它们统统在同一个default sta线程里面运行。

2. MTA客户调用STA COM对象的代理。

 

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

 

我们接下来就讲讲COM对象的跨线程传递和消息循环。



你可能感兴趣的:(COM)