簡序
大學畢業前的最後一學期,在一家公司實習,當時的工作需要用 到一些操作系統提供的組件。那時候只知道COM這個名詞,並不知道到底是怎麼 回事,只知道上網到處找別人的源碼解決自己的問題;那段日子到現在回憶起來 都是灰色的,每天呆坐在電腦前,一個網站一個網站的查找自己需要的源碼。但 並不清楚自己到底在做什麼;那時候對自己能不能成為一個程序員充滿了懷疑。 在實習結束返校的火車上,一夜間,我把一本《COM本質論》翻看了120多頁。當 我和當時的女友吹噓自己一夜可以看100多頁書的時候,她馬上問我:看懂多少 ?當時我啞口無言。她忍受不了我那段日子的失落和抱怨,從那時候起,我們結 束了那段簡短的感情。到如今我還在一個人漂泊著,而上周她成為了別人的妻子 。想不到用什麼方式去紀念我迄今為止經歷過的唯一一段感情,我和她的感情並 不完全是因為COM結束的,但由於對COM的迷惑,使我走向了迷茫,失落;對自己 失去了信心,在她面前變成了一個悲觀失望的人。寫這篇文章權當對這份感情的 一份紀念吧。
企者不立,跨著不行。很多格言都告訴我們做什麼事情都 必須從基礎開始,對COM的理解也是這個道理。當三年前我看《COM 本質論》的 時候,對虛函數也只是一知半解,只是知道通過它可以實現多態。但到底怎麼實 現就不清楚了。看不懂COM太正常了。知道看過Stanley B.Lippman的《Inside the C++ Object Model》,對C++的內存結構有了基本的理解,我才明白了接口 的意義。這篇文章是寫給初學者的,順便給大家一些建議,如果一本書你看不懂 的時候,可以先放放,先找一些基礎的讀物來看看。這樣可以少走一些彎路。
Don Box 在《COM 本質論》中說,對接口,類對象和套間有了徹底的理 解,那麼使用COM,沒有翻不過去的山頭。如果你對C++有深入的理解,那麼 《COM本質論》中對接口和類對象的闡述很清晰,理解並不困難。但套間是一個 比較抽象的概念,而書上對這部分只是理論的敘述,沒有提供具體的例子,理解 起來就更困難了。在此我把自己找到的一些例子和自己的理解總結以下,以期給 初學者提供一些入門的方法。閒話打住,開始正文吧。
一、關於多線程 (Multithreading)
子曰:本立道生。也就是說我們明白事物所存在的 原因,自然也就明白事物是怎麼回事了。如果我們清楚了套間(Apartment)的產 生原因,再去理解套間,就容易許多了。我們先來看看,為什麼需要套間?套間 是為解決多線程中使用組件而產生的,首先我們來了解一下多線程。
1、 理解進程(Processes)和線程(Threading)
理解線程,先從進程 (Processes)開始,一般書上對進程的描述都比較抽象,都說進程是一個運行 的程序的實例,進程擁有內存,資源。我這兒試著用一段匯編程序來解釋一下進 程,看看能不能幫你加深一下印象。我們先來看一段簡單的匯編程序(你不理解 匯編的話,建議找本書看看,一點不懂匯編,很難對其它高級語言有太深的理解 )。 ; 匯編程序示例
data_seg segment ;定義數據段
n_i dw ?
data_seg ends
stack_seg segment ;定義堆棧
dw 128 dup(0)
tos label word
statck_seg ends
code1 segment ;定義代 碼段
main proc far
assume cs:ccode,ds;data,seg,ss:stack_seg
start:
move ax,stack_seg ;將定義的堆棧段的地址保存到ss
mov ss,ax
mov sp,offset tos ;將堆棧的最後地址保存到sp,堆棧是從下到上 訪問的
push ds ;保存舊的數據段
sub ax,ax
push ax
mov ax,data_seg ;將定義 的數據段保存到ds
mov ds,ax
call fact ;調用子函數
……. ; 其它操作省略
ret ;返回到系統
main endp
fact proc near ;子函數定義
…… ;具體操作省略
ret ; 返回到調用處
fact endp
code1 ends
end start
示例1:匯編程序結構
從以上程 序我們看到,一個程序可以分為代碼段,數據段,堆棧段等幾部分。匯編編譯器 在編譯的時候會將這些文件轉化為成一個標准格式(在windows下被稱為PE文件 格式)的文件(很多時候可執行文件被命名為二進制文件,我不喜歡這個名字, 我覺得它容易給人誤解;事實上計算機上所有的文件都是0和1組成的,都是二進 制文件;真正不同的就是處理這些文件的方式;EXE文件需要操作系統來調用, TXT文件需要寫字本來打開;但其本質上並沒有什麼不同,只是在不同的組合上 ,二進制數有不同的意義)。該文件格式會把我們的代碼按格式安放在不同的部 分。程序必須在內存中,才可以執行。在程序運行前,操作系統會按照標准格式 將這些內容加載到內存中。這些數據加載到內存中也需要按照一定的格式,CPU 提供了DS,CS,SS等段寄存器,這樣代碼段的開始位置需要被CS指定,數據段的 開始位置需要用DS來指定,SS需要指向堆棧的開始位置等。在DOS下,每次只能 運行一個程序,這些內容基本構成了進程。但在Windows下,豐富了進程的內容 ,還包括一些數據結構用來維護我們程序中用到的圖標,對話框等內容,以及線 程。其實進程就是程序在內存中的組織形式,有了這樣的組織形式,程序才可能 運行。也就是說,當程序加載到內存中去後,就形成了一個進程。
我們 知道,CPU中擁有眾多的寄存器,EAX,EBX等,而CPU的指令一般都是通過寄存器 來實現的。其中有一個寄存器叫做EIP(Instruction Pointer,指令寄存器), 程序的有序執行,是靠它來完成的。看下面的例子: ……
mov eax,4
mov ebx,5
……
假如我們的程序運行到mov eax,4,那麼EIP就 會指向該句代碼所在的內存的地址。當這行代碼執行完畢之後,那麼EIP會自動 加一,那麼它就會指向mov ebx,4。而程序的執行就是靠EIP的不斷增加來完成 的(跳轉的話,EIP就變成了跳轉到的地址)。在Windows系統下,進程並不擁有 EIP,EAX,那麼只有進程,一個程序就無法運行。而擁有這些寄存器的是線程, 所以說進程是靜態的。
我們知道一個CPU下只有一個EIP,一個EAX,也就 是說同一時刻只能有一個線程可以運行,那麼所說的多線程又是什麼呢?事實上 同一時刻也只有一個線程在運行,每個線程運行一段時間後,它會把它擁有的 EIP,EAX等寄存器讓出來,其它線程占有這些寄存器後,繼續運行。因為這段時 間很短,所以我們感覺不出來。這樣我們就可以在一邊聽音樂的時候,一邊玩俄 羅斯方塊了。為了實現不同的線程之間的轉換,CPU要求操作系統維護一份固定 格式的數據(該數據存在於內存中),這份數據叫做Task-State Segment(TSS) ,在這份數據結構裡,維護著線程的EAX,EIP,DS等寄存器的內容。而CPU還有 一個寄存器叫做Task Register(TR),該寄存器指向當前正在執行的線程的TSS 。而線程切換事實上就是TR指向不同的TSS,這樣CPU就會自動保存當前的EAX, EBX的信息到相應的TSS中,並將新的線程的信息加載到寄存器。
事實上 線程不過上一些數據結構,這些結構保存了程序執行時候需要的一些信息。我們 可以在windows提供的頭文件中找到一些影子,安裝VC後在它的include目錄下有 一個Winnt.h文件。在該文件中,我們可以找到這樣一個struct(_CONTEXT)。這 就是線程切換時需要的數據結構(我不確定Windows內部是否用的就是這個結構 ,但應該和這份數據相差無幾)。
//
// Context Frame
//
// This frame has a several purposes: 1) it is used as an argument to
// NtContinue, 2) is is used to constuct a call frame for APC delivery,
// and 3) it is used in the user level thread creation routines.
//
// The layout of the record conforms to a standard call frame.
//
typedef struct _CONTEXT {
//
// The flags values within this flag control the contents of
// a CONTEXT record.
//
// If the context record is used as an input parameter, then
// for each portion of the context record controlled by a flag
// whose value is set, it is assumed that that portion of the
// context record contains valid context. If the context record
// is being used to modify a threads context, then only that
// portion of the threads context will be modified.
//
// If the context record is used as an IN OUT parameter to capture
// the context of a thread, then only those portions of the thread''s
// context corresponding to set flags will be returned.
//
// The context record is never used as an OUT only parameter.
//
DWORD ContextFlags;
//
// This section is specified/returned if CONTEXT_DEBUG_REGISTERS is
// set in ContextFlags. Note that CONTEXT_DEBUG_REGISTERS is NOT
// included in CONTEXT_FULL.
//
DWORD Dr0;
DWORD Dr1;
DWORD Dr2;
DWORD Dr3;
DWORD Dr6;
DWORD Dr7;
//
// This section is specified/returned if the
// ContextFlags word contians the flag CONTEXT_FLOATING_POINT.
//
FLOATING_SAVE_AREA FloatSave;
//
// This section is specified/returned if the
// ContextFlags word contians the flag CONTEXT_SEGMENTS.
//
DWORD SegGs;
DWORD SegFs;
DWORD SegEs;
DWORD SegDs;
//
// This section is specified/returned if the
// ContextFlags word contians the flag CONTEXT_INTEGER.
//
DWORD Edi;
DWORD Esi;
DWORD Ebx;
DWORD Edx;
DWORD Ecx;
DWORD Eax;
//
// This section is specified/returned if the
// ContextFlags word contians the flag CONTEXT_CONTROL.
//
DWORD Ebp;
DWORD Eip;
DWORD SegCs; // MUST BE SANITIZED
DWORD EFlags; // MUST BE SANITIZED
DWORD Esp;
DWORD SegSs;
//
// This section is specified/returned if the ContextFlags word
// contains the flag CONTEXT_EXTENDED_REGISTERS.
// The format and contexts are processor specific
//
BYTE ExtendedRegisters[MAXIMUM_SUPPORTED_EXTENSION];
} CONTEXT;
好了,線程就先講這麼多了。如果對進程和線程的內容感 興趣,可以到Intel的網站下載PDF格式的電子書《IA-32 Intel Architecture Software Developer’s Manual》,紙版的書也可以在這兒預定(他們會 免費郵寄給你)。通過這套書,你可以對CPU的結構有一個清晰的認識。另外可 以找幾本講解Windows系統的書看看,不過這類的好書不多,最著名的是 《Advance Windows》,不過也是偏向於實用,對系統結構的講解不多。也是, 要完全去了解這部分的細節,太困難了,畢竟微軟沒有給我們提供這部分的源碼 。幸好,其實我們理解它大致的原理就足夠用了。
2、多線程存在的問題
我們首先看一段多線程程序(該程序可以在Code的MultiThreading中找 到):
#include <iostream>
#include <windows.h>
int g_i = 10; //一個全局變量
DWORD WINAPI ThreadProc(LPVOID lpv)
{
g_i += 10;
std::cout <<"In the Thread " << ::GetCurrentThreadId() << ",the first g_i is " << g_i << "!" << std::endl;
Sleep(5000); //睡眠
g_i += 10;
std::cout <<"In the Thread " << ::GetCurrentThreadId() << ",the secend g_i is " << g_i << "!" << std::endl;
return 0;
}
int main(int argc, char* argv [])
{
DWORD threadID[2];
HANDLE hThreads[2];
for(int i = 0; i <= 1; i++ ) //創建兩個線程
hThreads[i] = ::CreateThread(NULL,
0,
ThreadProc,
NULL,
0,
&threadID[i]);
WaitForMultipleObjects (2,hThreads,TRUE,INFINITE); //等待線程結束
for(i = 0; i <= 1; i++ )
::CloseHandle(hThreads[i]); //關閉線程句柄
system("pause");
return 0;
}
示例程序2-多線程程序
這段程序的本意是讓全局變量累次加10,並打印出操作後的數值。但我們運行程 序後的結果如下,可以看到程序的運行結果非我們所願。打印出的結果是一串亂 序的文字。
如何 解決這個問題呢?我們需要利用同步機制來控制我們的多線程程序,現在我們使 用臨界區來解決這個問題。代碼如下:(在Code的MultiThreading中將進入臨界 區和離開臨界區的代碼前的注釋去掉就可以了)
#include <iostream>
再次運行,結果就是我們所需要的了。
#include <windows.h>
int g_i = 10; //一個全局變量
CRITICAL_SECTION cs; // 一個臨界區變量
DWORD WINAPI ThreadProc(LPVOID lpv)
{
EnterCriticalSection(&cs); //進入臨界區
g_i += 10;
std::cout <<"In the Thread " << ::GetCurrentThreadId() << ",the first g_i is " << g_i << "!" << std::endl;
::LeaveCriticalSection(&cs);
Sleep(5000); //睡眠
EnterCriticalSection(&cs);
g_i += 10;
std::cout <<"In the Thread " << ::GetCurrentThreadId() << ",the secend g_i is " << g_i << "!" << std::endl;
::LeaveCriticalSection(&cs);
return 0;
}
int main(int argc, char* argv[])
{
DWORD threadID[2];
HANDLE hThreads[2];
InitializeCriticalSection(&cs);
for(int i = 0; i <= 1; i++ ) //創建兩個線程
hThreads[i] = ::CreateThread(NULL,
0,
ThreadProc,
NULL,
0,
&threadID[i]);
WaitForMultipleObjects(2,hThreads,TRUE,INFINITE); // 等待線程結束
for(i = 0; i <= 1; i++ )
::CloseHandle(hThreads[i]); //關閉線程句柄
system("pause");
return 0;
}
如上 所示我們通過在代碼中加入EnterCriticalSection和LeaveCriticalSection來實 現對數據的保護,如果我們只在程序開頭和結尾填加這兩個函數的話,也不會太 復雜,但是這樣也就失去了多線程的意義。程序不會更快,反而會變慢。所以我 們必須在所有需要保護的地方,對我們的操作進行保護。程序如果龐大的話,這 將是一個煩瑣而枯燥的工作,而且很容易出錯。如果是我們自己使用的類的話, 我們可以選擇不使用多線程,但組件是提供給別人用的。開發者無法阻止組件使 用者在多線程程序中使用自己提供的組件,這就要求組件必須是多線程安全的。 但並不是每個開發者都願意做這樣的工作,微軟的COM API設計者為了平衡這個 問題,就提出了套間的概念。
注意:以上只是一個簡單的例子,事實上 多線程中需要保護的部分一般集中在全局數據和靜態數據之上,因為這樣的數據 每個進程只有一份,如上所示的g_i。(想對多線程程序有更深入的認識,可以 找侯捷翻譯的《Win32多線程程序設計》看看,90年代出的書,到現在還暢銷, 足可以說明它的價值)
二、套間所要解決的問題
從多線程的描述 中,我們知道,套間所要解決的問題是幫助組件的開發者在實現多線程下調用組 件時候的同步問題。我們還是先看一段簡短的程序。
我們首先使用ATL創 建一個簡單的組件程序,該程序有一個接口(ITestInterface1),該接口支持一 個方法TestFunc1。(該組件可以在附加的源碼的 “Apartment\TestComObject1”目錄下找到)我們通過以下的程序調 用該組件。(該程序可以在附加的源碼的 “Apartment\ErrorUseApartment”目錄下找到)
#define _WIN32_WINNT 0x0400
該段程序將main中定義的 ITestInterface1對象,通過指針傳到了新建的線程中。運行該段程序,結果如 下,又是一串亂序的文字串。也就是說我們需要在TestComObject1中對 TestFunc1進行線程同步控制。但大多數人並不想這樣做,因為我們開發的組件 大多數情況下並不會在多線程執行。但為了避免低概率事件發生後的不良後果, 套間出場了。
#include <windows.h>
#include <iostream>
#include "..\TestComObject1\TestComObject1_i.c"
#include "..\TestComObject1\TestComObject1.h"
DWORD WINAPI ThreadProc(LPVOID lpv)
{
HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
if ( FAILED(hr) )
{
std::cout << "CoinitializeEx failed!" << std::endl;
return 0;
}
ITestInterface1 *pTest = NULL;
hr = ::CoCreateInstance(CLSID_TestInterface1,
0,
CLSCTX_INPROC,
IID_ITestInterface1,
(void**)&pTest);
if ( FAILED(hr) )
{
std::cout << "CoCreateInstance failed!" << std::endl;
return 0;
}
hr = pTest->TestFunc1();
if ( FAILED(hr) )
{
std::cout << "TestFunc1 failed!" << std::endl;
return 0;
}
pTest->Release();
::CoUninitialize();
return 0;
}
int main(int argc, char* argv[])
{
HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
if ( FAILED(hr) )
{
std::cout << "CoinitializeEx failed!" << std::endl;
return 0;
}
ITestInterface1 *pTest = NULL;
hr = ::CoCreateInstance (CLSID_TestInterface1,
0,
CLSCTX_INPROC,
IID_ITestInterface1,
(void**)&pTest);
if ( FAILED(hr) )
{
std::cout << "CoCreateInstance failed!" << std::endl;
return 0;
}
DWORD threadID;
HANDLE hThreads = ::CreateThread(NULL, //創建一個進程
0,
ThreadProc,
NULL, //將pTest作為一個參數傳 入新線程
0,
&threadID);
hr = pTest->TestFunc1();
if ( FAILED(hr) )
{
std::cout << "TestFunc1 failed!" << std::endl;
return 0;
}
::WaitForSingleObject(hThreads,INFINITE); //等待線程結束
::CloseHandle(hThreads); // 關閉線程句柄
pTest->Release();
::CoUninitialize();
system("pause");
return 0;
}
三、 套間如何實現數據的同步
我們已經知道套間的目的是用來實現數據的同 步,那麼套間如何來實現呢?如果我們能保證COM對象中的函數只能在該對象中 的另一個函數執行完以後,才能開始執行(也就是說組件中的函數只能一個一個 的執行),那麼我們的問題就可以解決了。是的,你可以發現,這樣的話,就失 去了多線程的優勢;但套間的目的是保證小概率下的線程安全,損耗一些性能, 應該比出現邏輯錯誤強點。
那麼又如何保證同一對象下的所有方法都必 須按順序逐個執行呢?微軟的COM API設計者們借用了Windows的消息機制。我們 先來看一下windows的消息機制圖。
我們 可以看到所有線程發出的消息都回首先放到消息隊列中,然後在通過消息循環分 發到各自窗口去,而消息隊列中的消息只能一個處理完後再處理另一個,借助消 息機制,就可以實現COM的函數一個一個的執行,而不會同時運行。Windows的消 息機制是通過窗口來實現的,那麼一個線程要接收消息,也應該有一個窗口。 COM API的設計者在它們的API函數中實現了一個隱藏的窗口。在我們調用 CoInitializeEx(NULL, COINIT_APARTMENTTHREADED)的時候,會生成這個窗口。 (如果你對softice等動態調試工具熟悉的話,可以通過跟蹤源碼來跟蹤 CoInitializeEx函數,可以發現它會調用API函數CreateWindowEx)。該窗口是 隱藏的,有了這個窗口,就可以支持消息機制,就有辦法來實現對象中函數的逐 一執行。這樣當對象指針被傳到其它線程的時候,從外部調用該對象的方法的時 候,就會先發一個消息到原線程,而不再直接訪問對象了。套間的原理大致就是 這樣。我們再來看看COM中的套間類型。
四、套間的類型
我們 首先看看ATL為我們提供的線程類型:Single,Apartment,Both,Free。我們還 是通過例子來說明它們的不同。我們仍然用我們使用剛才實現的TestComObject1 來進行測試,先對它實現的唯一方法進行一下說明。
STDMETHODIMP CTestInterface1::TestFunc1()
{
// TODO: Add your implementation code here
std::cout << "In the itestinferface1''s object, the thread''s id is " << ::GetCurrentThreadId() << std::endl;
return S_OK;
}
該方 法非常簡單,就是打印出該方法運行時,所在的線程的ID號。如果在不同的線程 中調用同一個對象的時候,通過套間,發送消息,最終該對象只應該在一個線程 中運行,所以它的線程ID號應該是相同的。我們將通過該ID值來驗證套間的存在 。
1、Single
先來看我們的示例程序(在 Code/Apartment/SingleApartment目錄下可以找到該工程):
#define _WIN32_WINNT 0x0400
#include <windows.h>
#include <iostream>
#include "..\TestComObject1\TestComObject1_i.c"
#include "..\TestComObject1\TestComObject1.h"
DWORD WINAPI ThreadProc(LPVOID lpv)
{
HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
if ( FAILED(hr) )
{
std::cout << "CoinitializeEx failed!" << std::endl;
return 0;
}
ITestInterface1 *pTest = NULL;
hr = ::CoCreateInstance(CLSID_TestInterface1,
0,
CLSCTX_INPROC,
IID_ITestInterface1,
(void**)&pTest);
if ( FAILED(hr) )
{
std::cout << "CoCreateInstance failed!" << std::endl;
return 0;
}
hr = pTest->TestFunc1();
if ( FAILED(hr) )
{
std::cout << "TestFunc1 failed!" << std::endl;
return 0;
}
pTest->Release();
::CoUninitialize();
return 0;
}
int main(int argc, char* argv[])
{
HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
if ( FAILED(hr) )
{
std::cout << "CoinitializeEx failed!" << std::endl;
return 0;
}
ITestInterface1 *pTest = NULL;
hr = ::CoCreateInstance (CLSID_TestInterface1,
0,
CLSCTX_INPROC,
IID_ITestInterface1,
(void**)&pTest);
if ( FAILED(hr) )
{
std::cout << "CoCreateInstance failed!" << std::endl;
return 0;
}
hr = pTest->TestFunc1();
if ( FAILED(hr) )
{
std::cout << "TestFunc1 failed!" << std::endl;
return 0;
}
DWORD threadID;
HANDLE hThreads[1];
hThreads[0] = ::CreateThread (NULL, //創建一個進程
0,
ThreadProc,
(LPVOID)pTest, //將pTest作為一個參數傳入 新線程
0,
&threadID);
::WaitForSingleObject(hThreads,INFINITE); //等待線程結束
::CloseHandle(hThreads); //關閉線程句柄
pTest->Release();
::CoUninitialize();
system("pause");
return 0;
}
以下是運行結果:
可以 看到,在main中我們創建了一個ITestInterface1接口對象,並調用TestFunc1, 此處會輸出一個線程ID——ThreadID1。之後主線程生成一個線程, 在該線程中,我們會再次生成一個ITestInterface1接口對象,此處再次調用 TestFunc1,可以看到輸出了另一個線程ID——ThreadID2。因為是不 同的對象,所以它們的線程ID號不同。(注意了,此處並沒有跨線程調用對象, 並不在套間的保護范圍)
好了,我們該來看看Single類型的套間了。如果 你和我一樣懶,不想為此去寫一個single類型的接口,那麼打開你的注冊表。
找到我們的接口ID,在InprocServer32項下,將ThreadingModel的值改為 Single,或者將該項刪除(這樣也代表是Single套間)。我們再來運行該程序, 再看運行結果。
當打 印出一個線程ID的時候,程序就停止了。Why?剛開始,我也被搞的頭暈腦脹。 到MSDN中查找WaitForSingleObject,原來WaitForSingleObject會破壞程序中的 消息機制,這樣在創建的線程中,TestFunc1需要通過消息機制來運行,消息機 制破壞,就無法運行了。哎!還的再改程序。在查查《Win32多線程程序設計》 ,原來在GUI中等待線程需要用MsgWaitForMultipleObjects。好的,我們需要重 新寫一個函數,專門用來實現消息同步。
DWORD ApartMentMsgWaitForMultipleObject(HANDLE *hHandle,DWORD dwWaitCout, DWORD dwMilliseconds)
{
BOOL bQuit = FALSE;
DWORD dwRet;
while(!bQuit)
{
int rc;
rc = ::MsgWaitForMultipleObjects
(
dwWaitCout, // 需要等待的對象數量
hHandle, // 對象樹組
FALSE, //等待 所有的對象
(DWORD)dwMilliseconds, // 等待的 時間
(DWORD)(QS_ALLINPUT | QS_ALLPOSTMESSAGE) // 事件類型
);
// 等待的事件激發
if( rc == WAIT_OBJECT_0 )
{
dwRet = rc;
bQuit = TRUE;
}
//其他 windows消息
else if( rc == WAIT_OBJECT_0 + dwWaitCout )
{
MSG msg;
while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
{
TranslateMessage (&msg);
DispatchMessage(&msg);
}
}
}
return dwRet;
}
該函數用來處理消息的同步, 也夠麻煩的,還需要自己寫這段程序。這段程序的意思是如果等待的事件被激發 ,那麼設置bQuit為TURE,那麼退出消息循環。如果接收到其它的消息的話,再 分發出去。好了,把我們的程序再改一下:
// ::WaitForSingleObject(hThreads,INFINITE); //等待線程結束
ApartMentMsgWaitForMultipleObject (hThreads,1,INFINITE);
我們再來看一下運行結果。
我們 可以看到兩處調用TestFunc1,得到的線程ID是相同的。我們再通過VC的調試功 能來看看第二個TestFunc1的運行過程。我們在兩個TesfFunc1調用處設置斷點, 然後通過F11跟蹤進TestFunc1來看看它的調用過程。以下是在Main中的調用過程 。
通過 Call Stack,我們可以看到,此處是在main中直接調用的。我們再來看第二處調 用:
我們 可以看到TestFunc1的調用需要通過一連串的API方法來實現。你感興趣的話,可 以通過反匯編的方法來跟蹤一下這些API,看看它們具體實現了什麼,這裡我們 可以看到這些函數在dll中的大致位置,你可以使用W32DASM等反匯編工具打開這 些dll,大致研究一下這些函數。
好了,我們已經看到了Single套間的作 用。那麼Single套間究竟是什麼意思呢?就是說每個被標志為Single的接口,在 一個進程中只會存活在一個套間中。該套間就是進程創建的第一個套間。你可以 將Main中與pTest相關的代碼都去掉,只保留CoInitializeEx(NULL, COINIT_APARTMENTTHREADED)和線程的創建,再次運行該程序,可以發現創建線 程中的TestFunc1仍然是通過消息來實現的。
好了看過了Single,我們還 是在注冊表中,將ThreadingModel改為Apartment。通過修改注冊表就可以實現 對套間類型的控制,證明了套間和我們的程序本身沒有什麼關系,ATL的選項所 做的作用也只是通過它來添加注冊表。套間只是對系統的一種提示,由COM API 通過注冊表信息來幫我們實現套間。
2、Apartment
在第二部分( 套間所要解決的問題),我們曾經提供了一個不同線程共享接口對象的方法,該 方法是錯誤的(我們也可以通過程序阻止這種用法,稍候再敘)。此處我們提供 一種正確的做法。以下代碼在Apartment/Apartmenttest下可以找到。
#define _WIN32_WINNT 0x0400
#include <windows.h>
#include <iostream>
#include "..\TestComObject1\TestComObject1_i.c"
#include "..\TestComObject1\TestComObject1.h"
DWORD WINAPI ThreadProc(LPVOID lpv)
{
//HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
HRESULT hr = CoInitializeEx(NULL, COINIT_MULTITHREADED);
if ( FAILED(hr) )
{
std::cout << "CoinitializeEx failed!" << std::endl;
return 0;
}
IStream *pStream = (IStream*)lpv;
ITestInterface1 *pTest = NULL;
hr = ::CoGetInterfaceAndReleaseStream(pStream,
IID_ITestInterface1,
(void**)&pTest);
if ( FAILED(hr) )
{
std::cout << "CoGetInterfaceAndReleaseStream failed!" << std::endl;
return 0;
}
hr = pTest->TestFunc1();
if ( FAILED(hr) )
{
std::cout << "TestFunc1 failed!" << std::endl;
return 0;
}
pTest->Release();
::CoUninitialize();
return 0;
}
DWORD ApartMentMsgWaitForMultipleObject(HANDLE *hHandle,DWORD dwWaitCout, DWORD dwMilliseconds)
{
BOOL bQuit = FALSE;
DWORD dwRet;
while(! bQuit)
{
int rc;
rc = ::MsgWaitForMultipleObjects
(
dwWaitCout, // 需要等待的對象數量
hHandle, // 對象樹組
FALSE, //等待所有的對象
(DWORD)dwMilliseconds, // 等待的時間
(DWORD)(QS_ALLINPUT | QS_ALLPOSTMESSAGE) // 事件類型
);
if( rc == WAIT_OBJECT_0 )
{
dwRet = rc;
bQuit = TRUE;
}
else if( rc == WAIT_OBJECT_0 + dwWaitCout )
{
MSG msg;
while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
{
TranslateMessage (&msg);
DispatchMessage(&msg);
}
}
}
return dwRet;
}
int main(int argc, char* argv[])
{
//HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
HRESULT hr = CoInitializeEx(NULL, COINIT_MULTITHREADED);
if ( FAILED(hr) )
{
std::cout << "CoinitializeEx failed!" << std::endl;
return 0;
}
ITestInterface1 *pTest = NULL;
hr = ::CoCreateInstance(CLSID_TestInterface1,
0,
CLSCTX_INPROC,
IID_ITestInterface1,
(void**)&pTest);
if ( FAILED(hr) )
{
std::cout << "CoCreateInstance failed!" << std::endl;
return 0;
}
hr = pTest->TestFunc1();
if ( FAILED(hr) )
{
std::cout << "TestFunc1 failed!" << std::endl;
return 0;
}
IStream *pStream = NULL;
hr = ::CoMarshalInterThreadInterfaceInStream(IID_ITestInterface1,
pTest,
&pStream);
if ( FAILED(hr) )
{
std::cout << "CoMarshalInterThreadInterfaceInStream failed!" << std::endl;
return 0;
}
DWORD threadID;
HANDLE hThreads[1];
hThreads[0] = ::CreateThread(NULL, //創建一個進程
0,
ThreadProc,
(LPVOID)pStream, //將pStream作為一個參數傳 入新線程
0,
&threadID);
ApartMentMsgWaitForMultipleObject (hThreads,1,INFINITE);
::CloseHandle(hThreads); //關閉線程句柄
pTest->Release();
::CoUninitialize();
system("pause");
return 0;
}
我們通過 CoGetInterfaceAndReleaseStream將main中的pTest變為pStream,然後將 pStream作為參數傳入到線程中,然後再通過CoGetInterfaceAndReleaseStream 將pSteam變為接口指針。再來看看運行的結果:
可以 看到兩次運行,線程ID是相同的。好的,我們接著改變注冊表,再將Apartment 變為Free。然後再將兩處的HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);改為HRESULT hr = CoInitializeEx(NULL, COINIT_MULTITHREADED)。編譯後再次執行該程序,再來看執行結果。
我們 可以看到兩個線程的ID是不同的。你可以通過VC的Debug來看這兩組程序的 TesFunc1的調用情況,在第二種情況下,創建的線程中不會通過消息機制來調用 該函數。
通過對比,我們可以知道所說的套間,就是通過消息機制來控 制不同線程中對對象的調用。這樣就不需要組件的實現者來實現數據的同步。
3、Free
上節的例子,已經為我們提示了我們Free套間,其實系 統對我們的組件不做控制,這樣就需要組件的開發者對數據的同步做出控制。
4、Both
所謂Both,就是說該對象既可以運行在Apartment中,也 可以運行在Free套間中。該類型的前提是它應該是Free類型的套間,也就是說組 件自己實現了數據的同步。然後設置成Both類型。
為什麼需要Both類型 的套間呢?想想假如我們在我們的組件中調用另一個組件,這樣我們就需要在我 們的組件中為所調用的組件來開辟一個套間。我們的套間是一個Apartment,而 調用的組件是Free類型的,這樣這兩個對象就必須存在於不同的兩個套間中。而 跨套間的調用,需要通過中間代理來實現,這樣必然會損失性能。但如果我們調 用的套間類型是Both的話,它就可以和我們的組件同享一個套間,這樣就可以提 高效率。
五、缺省套間
繼續我們的測試,首先在注冊表中將我們 的接口類型改回Apartment。然後新建一個工程DefaultApartment。C++文件中的 實現代碼如下。
#define _WIN32_WINNT 0x0400
#include <windows.h>
#include <iostream>
#include "..\TestComObject1\TestComObject1_i.c"
#include "..\TestComObject1\TestComObject1.h"
DWORD WINAPI ThreadProc(LPVOID lpv)
{
HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
//HRESULT hr = CoInitializeEx(NULL, COINIT_MULTITHREADED);
if ( FAILED(hr) )
{
std::cout << "CoinitializeEx failed!" << std::endl;
return 0;
}
IStream *pStream = (IStream*)lpv;
ITestInterface1 *pTest = NULL;
hr = ::CoGetInterfaceAndReleaseStream (pStream,
IID_ITestInterface1,
(void**)&pTest);
if ( FAILED(hr) )
{
std::cout << "CoGetInterfaceAndReleaseStream failed!" << std::endl;
return 0;
}
std::cout << "ThradProc''s threadid is " << ::GetCurrentThreadId() << std::endl; //輸出ThradProc的 線程ID
hr = pTest->TestFunc1();
if ( FAILED(hr) )
{
std::cout << "TestFunc1 failed!" << std::endl;
return 0;
}
pTest->Release ();
::CoUninitialize();
return 0;
}
DWORD ApartMentMsgWaitForMultipleObject(HANDLE *hHandle,DWORD dwWaitCout, DWORD dwMilliseconds)
{
BOOL bQuit = FALSE;
DWORD dwRet;
while(!bQuit)
{
int rc;
rc = ::MsgWaitForMultipleObjects
(
dwWaitCout, // 需要等待的對象數量
hHandle, // 對象樹組
FALSE, //等待所有的對象
(DWORD) dwMilliseconds, // 等待的時間
(DWORD) (QS_ALLINPUT | QS_ALLPOSTMESSAGE) // 事件類型
);
if( rc == WAIT_OBJECT_0 )
{
dwRet = rc;
bQuit = TRUE;
}
else if( rc == WAIT_OBJECT_0 + dwWaitCout )
{
MSG msg;
while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
{
TranslateMessage (&msg);
DispatchMessage(&msg);
}
}
}
return dwRet;
}
int main(int argc, char* argv[])
{
HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
//HRESULT hr = CoInitializeEx (NULL, COINIT_MULTITHREADED);
if ( FAILED(hr) )
{
std::cout << "CoinitializeEx failed!" << std::endl;
return 0;
}
ITestInterface1 *pTest = NULL;
hr = ::CoCreateInstance(CLSID_TestInterface1,
0,
CLSCTX_INPROC,
IID_ITestInterface1,
(void**)&pTest);
if ( FAILED(hr) )
{
std::cout << "CoCreateInstance failed!" << std::endl;
return 0;
}
std::cout << "main''s threadid is " << ::GetCurrentThreadId() << std::endl; //打印main的線程ID
hr = pTest->TestFunc1();
if ( FAILED(hr) )
{
std::cout << "TestFunc1 failed!" << std::endl;
return 0;
}
IStream *pStream = NULL;
hr = ::CoMarshalInterThreadInterfaceInStream (IID_ITestInterface1,
pTest,
&pStream);
if ( FAILED(hr) )
{
std::cout << "CoMarshalInterThreadInterfaceInStream failed!" << std::endl;
return 0;
}
DWORD threadID;
HANDLE hThreads[1];
hThreads[0] = ::CreateThread(NULL, //創建一個進程
0,
ThreadProc,
(LPVOID)pStream, //將pStream作為一個參數傳 入新線程
0,
&threadID);
ApartMentMsgWaitForMultipleObject (hThreads,1,INFINITE);
::CloseHandle(hThreads); //關閉線程句柄
pTest->Release();
::CoUninitialize();
system("pause");
return 0;
}
此部分代碼與我們測試Apartment時的 代碼基本相同,只是新增了輸出main和創建線程的ID的語句。好的,我們來運行 程序,可以得到如下的結果:
我們 可以看到main的線程ID和兩個TestFunc1的線程ID相同。也就是說兩個TestFunc1 都是在main的線程中運行的。
將我們的程序做些變動,將 CoInitializeEx(NULL, COINIT_APARTMENTTHREADED)改為 CoInitializeEx (NULL, COINIT_MULTITHREADED)。然後接著運行程序。我們再來看運行的結果。
我們可以看到兩個TestFunc1的線程ID和main的不同了,和我們創建的線程 也不同。這是為什麼呢?CoInitializeEx是一個創建套間的過程,我們使用 CoInitializeEx(NULL, COINIT_MULTITHREADED)後,沒有為我們的組件創建合適 的套間。這時候系統(也就是COM API,這裡應該是通過CoCreateInstance來實 現的)就會幫我們將我們的接口對象放入缺省套間,該套間並不運行在當前的線 程中。我們再次在Debug下跟蹤運行過程,可以發現在main中調用TestFunc1,也 需要通過眾多的API函數幫助完成,也就是說此處也是通過消息機制來完成的, 這樣性能上肯定會有影響。
六、阻止接口指針的非法使用
在第二 部分我們給出了一個通過直接傳輸接口指針到另外線程的例子,事實上這種方法 是錯誤的,但COM API並沒有幫助我們阻止這樣的錯誤。這個任務可以由我們自 己來完成。
因為套間是和線程相關的,Apartment類型的接口方法只應該 運行在一個套間中(其實這就是一個協議,並不是強制性的),那麼我們可以通 過線程的相關性質來實現。
在線程中我們可以通過Thread Local Storage(TLS)來保存線程的相關信息,同一函數運行在不同的線程中,那麼它 所擁有的TLS也不相同。
我們來動手改造我們的類實現,將 CTestferface1進行改造。
class ATL_NO_VTABLE CTestInterface1 :
public CComObjectRootEx,
public CComCoClass,
public IDispatchImpl
{
private:
DWORD dwTlsIndex;
public:
CTestInterface1()
{
dwTlsIndex = TlsAlloc();
HLOCAL l = LocalAlloc(LMEM_FIXED, 1);
TlsSetValue(dwTlsIndex, l);
}
我們先聲明一個私有成員變量dwTlsIndex,它用來存放TLS的索引值(一個 線程的TLS相當於一個數組,可以存放不同的數據)。再將構造函數中填入保存 數據的代碼。此處只是簡單的分配了一個字節的地址,並將該地址通過 TlsSetValue保存到TLS中去。
然後再改造我們的TestFunc1函數。如下:
STDMETHODIMP CTestInterface1::TestFunc1()
{
// TODO: Add your implementation code here
LPVOID lpvData = TlsGetValue(dwTlsIndex);
if ( lpvData == NULL )
return RPC_E_WRONG_THREAD;
std::cout << "In the itestinferface1''s object, the thread''s id is " << ::GetCurrentThreadId() << std::endl;
return S_OK;
}
這 邊也很簡單,就是簡單的通過TlsGetValue去嘗試得到dwTlsIndex所標志的內容 是否存在。如果不存在,那麼就說明程序運行在了不同的套間中。就會返回 RPC_E_WRONG_THREAD,這是COM設計者定義的宏,表示線程的非法使用。(由於 我的懶惰,不再寫新的COM了,只是簡單的修改了TestComObject1,這部分新加 的代碼被我注釋掉了,你如果想看這部分的效果,去掉注釋就可以了)
我們再運行ErrorUseApartment程序,發現TestFunc1已經無法輸出線程號,而是 直接返回RPC_E_WRONG_THREAD。再次運行ApartmentTest程序,發現這樣的處理 對它並沒有影響。仍然正常運行。
六、什麼是套間?
我們從外部 表現上對套間進行了了解,而套間究竟是什麼?潘愛民譯的《Com 本質論》說: 套間既不是進程,也不是線程,然而套間擁有進程和線程的某些特性。我覺得, 這句話翻譯的不到位,總讓人感覺套間似乎是和進程或者線程等同的東西。找來 原文看看:An apartment is neither a process nor a thread; however, apartments share some of the properties of both。這裡的share被譯成了擁 有,但我感覺此處翻譯為使用或者分享可能更貼切一些。不過原文事實上也很容 易給初學者帶來誤導。其實套間只是保存在線程中的一個數據結構(還有一個隱 藏著的窗口),借用該結構使套間和線程之間建立起某種關系,通過該關系,使 得COM API通過該信息可以建立不同套間中的調用機制。這部分涉及到列集,散 集(我們調用CoMarshalInterThreadInterfaceInStream, CoGetInterfaceAndReleaseStream的過程)。在列集和散集過程中,COM API會 幫我們建立一個不同套間中對象通信機制,這部分涉及到了代理,存根和通道的 內容。通過代理來發送調用信息,通過通道發送到存根,再通過存根調用實際的 方法(其實那個隱藏的窗口就是為存根來服務的)。所做的這一切不過是為了實 現不同套間中可以通過消息來調用對象。你可以找《Com 本質論》來看看,這部 分的內容比較繁雜,但我感覺比起套間的概念,還是比較容易的。
具體 實現套間,在線程的TLS究竟保存了什麼信息呢?罪惡的微軟隱藏了這邊部分內 容,我們無法得到這部分的材料。這可能也是套間理解起來如此困難的一個原因 ,套間呈現給我們的是一個抽象的概念。但理解其實際意義後,抽不抽象已經沒 什麼關系,因為它所隱藏的不過是創建和使用套間時候繁雜的調用其它API函數 的過程,事實上並沒有太多的神秘可言。對我們開發者來說,能明白套間的意義 ,已經足夠了。
好了,稍微總結一下:套間是保存在線程的TLS中的一個 數據結構,通過該結構可以幫助不同的套間之間通過消息機制來實現函數的調用 ,以保證多線程環境下,數據的同步。
結語
石康說:比爾.蓋茨 並不是什麼天才,軟件工作者充其量不過是一個技術工作者,無法和科學工作者 同日而語。石康還說:如果給他老人家足夠的時間,他也可以寫出一個操作系統 。呵呵,大意好象如此,似乎是他老人家在《支離破碎》中的名言,現在記不太 清楚了。剛開始覺得他老人家太狂了,不過仔細體會一下,確實如此。計算機的 世界很少有真正高深的東西,有些內容你不理解,肯定是你的某方面的基礎不扎 實。不理解接口,那是因為你的C++沒學好;不理解套間,那是因為你不懂多線 程;不懂多線程那是因為你不懂CPU的結構。
技術革新在眼花缭亂的進行 的,.Net,Web services,到處閃現著新鮮的名詞,似乎這個世界每天都在變化 的。但事實上,從286到386,從dos到圖形操作系統後,計算機再沒有什麼重大的革新。從我們開發者的角度來看,不過是開發工具的更新。但每次開發工具的 更新都能使很多人興奮異常,激動著下載安裝最新版本的工具,追逐著學習最新 的開發語言。總覺的這樣就不會被時代所拋棄,總以為開發工具會幫著提升自己 的價值。事實上呢?學會拖拉創建窗口的人,可能根本不知道Windows中有一個 消息機制。開發十多年的人會把一個棧中生成的對象的地址作為參數傳給接收者 。沒有學會走的時候,不要去跑。我自己也在迷茫中探索著自己的路,現在有點 明白老子所說的“企者不立,跨者不行”。
好了,廢話就此 打住吧!只是想告訴你,其實編程並沒有那麼困難,如果有什麼東西沒明白,別 著急,找基礎的東西去看。學好COM也一樣,看不懂的話,先把C++中的虛函數學 明白,再去了解一下多線程的內容。其實也沒那麼復雜!
有人說,COM過 時了,我也不清楚COM的將來會怎麼樣,但我覺得理解一個東西總是有樂趣的。 與你同勉。
本文配套源碼