From : http://blog.csdn.net/zj510/article/details/38829069
前面一篇文章讲述了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;
- }
// 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;
- }
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;
- }
// 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;
- }
// 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