前面一篇文章讲述了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