原文地址:http://www.vckbase.com/index.php/wv/1315


簡序

大學畢業前的最后一學期,在一家公司實習,當時的工作需要用到一些操作系統提供的組件。那時候只知道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)開始,一般書上對進程的描述都比較抽象,都說進程是一個運行的程序的實例,進程擁有內存,資源。我這兒試著用一 段匯編程序來解釋一下進程,看看能不能幫你加深一下印象。我們先來看一段簡單的匯編程序(你不理解匯編的話,建議找本書看看,一點不懂匯編,很難對其它高 級語言有太深的理解)。

01.; 匯編程序示例
02.data_seg segment  ;定義數據段
03.n_i  dw   ?
04.data_seg ends
05. 
06.stack_seg segment ;定義堆棧
07.dw 128 dup(0)
08.tos label word
09.statck_seg ends
10. 
11.code1 segment   ;定義代碼段
12.main proc far
13.assume cs:ccode,ds;data,seg,ss:stack_seg
14.start:
15.move ax,stack_seg   ;將定義的堆棧段的地址保存到ss
16.mov ss,ax
17.mov sp,offset tos     ;將堆棧的最后地址保存到sp,堆棧是從下到上訪問的
18. 
19.push ds  ;保存舊的數據段
20.sub ax,ax
21.push ax
22. 
23.mov ax,data_seg     ;將定義的數據段保存到ds
24.mov ds,ax
25. 
26.call fact               ;調用子函數
27. 
28.…….             ;其它操作省略
29.ret     ;返回到系統
30.main endp
31. 
32.fact proc near       ;子函數定義
33. 
34.……              ;具體操作省略
35.ret  ;返回到調用處
36.fact endp
37. 
38.code1 ends
39.end start
40.示例1:匯編程序結構

從以上程序我們看到,一個程序可以分為代碼段,數據段,堆棧段等幾部分。匯編編譯器在編譯的時候會將這些文件轉化為成一個標準格式(在windows下被 稱為PE文件格式)的文件(很多時候可執行文件被命名為二進制文件,我不喜歡這個名字,我覺得它容易給人誤解;事實上計算機上所有的文件都是0和1組成 的,都是二進制文件;真正不同的就是處理這些文件的方式;EXE文件需要操作系統來調用,TXT文件需要寫字本來打開;但其本質上并沒有什么不同,只是在 不同的組合上,二進制數有不同的意義)。該文件格式會把我們的代碼按格式安放在不同的部分。程序必須在內存中,才可以執行。在程序運行前,操作系統會按照 標準格式將這些內容加載到內存中。這些數據加載到內存中也需要按照一定的格式,CPU提供了DS,CS,SS等段寄存器,這樣代碼段的開始位置需要被CS 指定,數據段的開始位置需要用DS來指定,SS需要指向堆棧的開始位置等。在DOS下,每次只能運行一個程序,這些內容基本構成了進程。但在 Windows下,豐富了進程的內容,還包括一些數據結構用來維護我們程序中用到的圖標,對話框等內容,以及線程。其實進程就是程序在內存中的組織形式, 有了這樣的組織形式,程序才可能運行。也就是說,當程序加載到內存中去后,就形成了一個進程。

我們知道,CPU中擁有眾多的寄存器,EAX,EBX等,而CPU的指令一般都是通過寄存器來實現的。其中有一個寄存器叫做EIP(Instruction Pointer,指令寄存器),程序的有序執行,是靠它來完成的。看下面的例子:

1.……
2.mov eax,4
3.mov ebx,5
4.……

假如我們的程序運行到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內部是否用的就是這個結構,但應該和這份數據相差無幾)。

01.//
02.// Context Frame
03.//
04.//  This frame has a several purposes: 1) it is used as an argument to
05.//  NtContinue, 2) is is used to constuct a call frame for APC delivery,
06.//  and 3) it is used in the user level thread creation routines.
07.//
08.//  The layout of the record conforms to a standard call frame.
09.//
10. 
11.typedef struct _CONTEXT {
12. 
13.//
14.// The flags values within this flag control the contents of
15.// a CONTEXT record.
16.//
17.// If the context record is used as an input parameter, then
18.// for each portion of the context record controlled by a flag
19.// whose value is set, it is assumed that that portion of the
20.// context record contains valid context. If the context record
21.// is being used to modify a threads context, then only that
22.// portion of the threads context will be modified.
23.//
24.// If the context record is used as an IN OUT parameter to capture
25.// the context of a thread, then only those portions of the thread''s
26.// context corresponding to set flags will be returned.
27.//
28.// The context record is never used as an OUT only parameter.
29.//
30. 
31.DWORD ContextFlags;
32. 
33.//
34.// This section is specified/returned if CONTEXT_DEBUG_REGISTERS is
35.// set in ContextFlags.  Note that CONTEXT_DEBUG_REGISTERS is NOT
36.// included in CONTEXT_FULL.
37.//
38. 
39.DWORD   Dr0;
40.DWORD   Dr1;
41.DWORD   Dr2;
42.DWORD   Dr3;
43.DWORD   Dr6;
44.DWORD   Dr7;
45. 
46.//
47.// This section is specified/returned if the
48.// ContextFlags word contians the flag CONTEXT_FLOATING_POINT.
49.//
50. 
51.FLOATING_SAVE_AREA FloatSave;
52. 
53.//
54.// This section is specified/returned if the
55.// ContextFlags word contians the flag CONTEXT_SEGMENTS.
56.//
57. 
58.DWORD   SegGs;
59.DWORD   SegFs;
60.DWORD   SegEs;
61.DWORD   SegDs;
62. 
63.//
64.// This section is specified/returned if the
65.// ContextFlags word contians the flag CONTEXT_INTEGER.
66.//
67. 
68.DWORD   Edi;
69.DWORD   Esi;
70.DWORD   Ebx;
71.DWORD   Edx;
72.DWORD   Ecx;
73.DWORD   Eax;
74. 
75.//
76.// This section is specified/returned if the
77.// ContextFlags word contians the flag CONTEXT_CONTROL.
78.//
79. 
80.DWORD   Ebp;
81.DWORD   Eip;
82.DWORD   SegCs;        // MUST BE SANITIZED
83.DWORD   EFlags;       // MUST BE SANITIZED
84.DWORD   Esp;
85.DWORD   SegSs;
86. 
87.//
88.// This section is specified/returned if the ContextFlags word
89.// contains the flag CONTEXT_EXTENDED_REGISTERS.
90.// The format and contexts are processor specific
91.//
92. 
93.BYTE    ExtendedRegisters[MAXIMUM_SUPPORTED_EXTENSION];
94. 
95.} CONTEXT;

好了,線程就先講這么多了。如果對進程和線程的內容感興趣,可以到Intel的網站下載PDF格式的電子書《IA-32 Intel Architecture Software Developer’s Manual》,紙版的書也可以在這兒預定(他們會免費郵寄給你)。通過這套書,你可以對CPU的結構有一個清晰的認識。另外可以找幾本講解 Windows系統的書看看,不過這類的好書不多,最著名的是《Advance Windows》,不過也是偏向于實用,對系統結構的講解不多。也是,要完全去了解這部分的細節,太困難了,畢竟微軟沒有給我們提供這部分的源碼。幸好, 其實我們理解它大致的原理就足夠用了。

2、多線程存在的問題

我們首先看一段多線程程序(該程序可以在Code的MultiThreading中找到):

01.#include < iostream >
02.#include < windows.h >
03. 
04.int g_i = 10;  //一個全局變量
05. 
06.DWORD WINAPI ThreadProc(LPVOID lpv)
07.{
08.g_i += 10;
09.std::cout <<"In the Thread " << ::GetCurrentThreadId() << ",the first g_i is "  <<  g_i  <<  "!"  << std::endl;
10.Sleep(5000); //睡眠
11.g_i += 10;
12.std::cout <<"In the Thread " << ::GetCurrentThreadId() << ",the secend g_i is "  <<  g_i  << "!" << std::endl;
13.return 0;
14.}
15. 
16.int main(int argc, char* argv[])
17.{
18. 
19.DWORD threadID[2];
20.HANDLE hThreads[2];
21. 
22.for(int i = 0; i <= 1; i++ )         //創建兩個線程
23.hThreads[i] = ::CreateThread(NULL,
24.        0,
25.        ThreadProc,
26.        NULL,
27.        0,
28.        &threadID[i]);
29. 
30. 
31.WaitForMultipleObjects(2,hThreads,TRUE,INFINITE);   //等待線程結束
32. 
33.for(i = 0; i <= 1; i++ )
34.::CloseHandle(hThreads[i]);             //關閉線程句柄
35.system("pause");
36.return 0;
37.}
38.示例程序2-多線程程序

這段程序的本意是讓全局變量累次加10,并打印出操作后的數值。但我們運行程序后的結果如下,可以看到程序的運行結果非我們所愿。打印出的結果是一串亂序的文字。 

 

如何解決這個問題呢?我們需要利用同步機制來控制我們的多線程程序,現在我們使用臨界區來解決這個問題。代碼如下:(在Code的MultiThreading中將進入臨界區和離開臨界區的代碼前的注釋去掉就可以了)

01.#include < iostream >
02.#include < windows.h >
03. 
04.int g_i = 10;  //一個全局變量
05. 
06.CRITICAL_SECTION cs;  //一個臨界區變量
07. 
08.DWORD WINAPI ThreadProc(LPVOID lpv)
09.{
10.EnterCriticalSection(&cs);  //進入臨界區
11. 
12.g_i += 10;
13.std::cout < <  "In the Thread " < <   ::GetCurrentThreadId() < <   ",the first g_i is "  < <   g_i < <    "!"  < <   std::endl;
14.::LeaveCriticalSection(&cs);
15.Sleep(5000); //睡眠
16.EnterCriticalSection(&cs);
17.g_i += 10;
18.std::cout < <    "In the Thread " < <  ::GetCurrentThreadId() < <  ",the secend g_i is "  < <  g_i < <  "!" < <  std::endl;
19.::LeaveCriticalSection(&cs);
20.return 0;
21.}
22. 
23.int main(int argc, char* argv[])
24.{
25. 
26.DWORD threadID[2];
27.HANDLE hThreads[2];
28.InitializeCriticalSection(&cs);
29.for(int i = 0; i < = 1; i++ )            //創建兩個線程
30.hThreads[i] = ::CreateThread(NULL,
31.    0,
32.    ThreadProc,
33.    NULL,
34.    0,
35.    &threadID[i]);
36. 
37.WaitForMultipleObjects(2,hThreads,TRUE,INFINITE);   //等待線程結束
38.for(i = 0; i < = 1; i++ )
39.::CloseHandle(hThreads[i]);             //關閉線程句柄
40. 
41.system("pause");
42.return 0;
43.}

再次運行,結果就是我們所需要的了。 

     

如上所示我們通過在代碼中加入EnterCriticalSection和LeaveCriticalSection來實現對數據的保護,如我們只在程序 開頭和結尾填加這兩個函數的話,也不會太復雜,但是這樣也就失去了多線程的意義。程序不會更快,反而會變慢。所以我們必須在所有需要保護的地方,對我們的 操作進行保護。程序如果龐大的話,這將是一個煩瑣而枯燥的工作,而且很容易出錯。如果是我們自己使用的類的話,我們可以選擇不使用多線程,但組件是提供給 別人用的。開發者無法阻止組件使用者在多線程程序中使用自己提供的組件,這就要求組件必須是多線程安全的。但并不是每個開發者都愿意做這樣的工作,微軟的 COM API設計者為了平衡這個問題,就提出了套間的概念。 

注意:以上只是一個簡單的例子,事實上多線程中需要保護的部分一般集中在全局數據和靜態數據之上,因為這樣的數據每個進程只有一份,如上所示的g_i。 (想對多線程程序有更深入的認識,可以找侯捷翻譯的《Win32多線程程序設計》看看,90年代出的書,到現在還暢銷,足可以說明它的價值)

二、套間所要解決的問題   

從多線程的描述中,我們知道,套間所要解決的問題是幫助組件的開發者在實現多線程下調用組件時候的同步問題。我們還是先看一段簡短的程序。

我們首先使用ATL創建一個簡單的組件程序,該程序有一個接口(ITestInterface1),該接口支持一個方法TestFunc1。(該組件可以 在附加的源碼的“Apartment\TestComObject1”目錄下找到)我們通過以下的程序調用該組件。(該程序可以在附加的源碼的 “Apartment\ErrorUseApartment”目錄下找到)

01.#define _WIN32_WINNT 0x0400
02.#include < windows.h >
03.#include < iostream >
04. 
05.#include "..\TestComObject1\TestComObject1_i.c"
06.#include "..\TestComObject1\TestComObject1.h"
07. 
08.DWORD WINAPI ThreadProc(LPVOID lpv)
09.{
10. 
11.HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
12. 
13.if ( FAILED(hr) )
14.{
15.std::cout << "CoinitializeEx failed!" << std::endl;
16.return 0;
17.}
18. 
19.ITestInterface1 *pTest = NULL;
20. 
21.hr = ::CoCreateInstance(CLSID_TestInterface1,
22.0,
23.CLSCTX_INPROC,
24.IID_ITestInterface1,
25.(void**)&pTest);
26. 
27.if ( FAILED(hr) )
28.{
29.std::cout << "CoCreateInstance failed!" << std::endl;
30.return 0;
31.}
32. 
33.hr = pTest->TestFunc1();
34. 
35.if ( FAILED(hr) )
36.{
37.std::cout << "TestFunc1 failed!" << std::endl;
38.return 0;
39.}
40. 
41.pTest->Release();
42.::CoUninitialize();
43.return 0;
44.}
45. 
46.int main(int argc, char* argv[])
47.{
48.HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
49. 
50.if ( FAILED(hr) )
51.{
52.std::cout << "CoinitializeEx failed!" << std::endl;
53.return 0;
54.}
55. 
56.ITestInterface1 *pTest = NULL;
57. 
58.hr = ::CoCreateInstance(CLSID_TestInterface1,
59.0,
60.CLSCTX_INPROC,
61.IID_ITestInterface1,
62.(void**)&pTest);
63. 
64.if ( FAILED(hr) )
65.{
66.std::cout << "CoCreateInstance failed!" << std::endl;
67.return 0;
68.}
69. 
70.DWORD threadID;
71.HANDLE hThreads  =   ::CreateThread(NULL, //創建一個進程
72.0,
73.ThreadProc,
74.NULL,  //將pTest作為一個參數傳入新線程
75.0,
76.&threadID);
77.hr = pTest->TestFunc1();
78. 
79.if ( FAILED(hr) )
80.{
81.std::cout << "TestFunc1 failed!" << std::endl;
82.return 0;
83.}
84. 
85.::WaitForSingleObject(hThreads,INFINITE);   //等待線程結束
86.::CloseHandle(hThreads);                //關閉線程句柄
87.pTest->Release();
88.::CoUninitialize();
89.system("pause");
90.return 0;
91.}

該段程序將main中定義的ITestInterface1對象,通過指針傳到了新建的線程中。運行該段程序,結果如下,又是一串亂序的文字串。也就是說 我們需要在TestComObject1中對TestFunc1進行線程同步控制。但大多數人并不想這樣做,因為我們開發的組件大多數情況下并不會在多線 程執行。但為了避免低概率事件發生后的不良后果,套間出場了。 

 

三、套間如何實現數據的同步

我們已經知道套間的目的是用來實現數據的同步,那么套間如何來實現呢?如果我們能保證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來進行測試,先對它實現的唯一方法進行一下說明。

1.STDMETHODIMP CTestInterface1::TestFunc1()
2.{
3.// TODO: Add your implementation code here
4.std::cout << "In the itestinferface1''s object, the thread''s id is " << ::GetCurrentThreadId() << std::endl;
5.return S_OK;
6.}

該方法非常簡單,就是打印出該方法運行時,所在的線程的ID號。如果在不同的線程中調用同一個對象的時候,通過套間,發送消息,最終該對象只應該在一個線程中運行,所以它的線程ID號應該是相同的。我們將通過該ID值來驗證套間的存在。

1、Single

先來看我們的示例程序(在Code/Apartment/SingleApartment目錄下可以找到該工程):

01.#define _WIN32_WINNT 0x0400
02.#include < windows.h >
03.#include < iostream >
04. 
05.#include "..\TestComObject1\TestComObject1_i.c"
06.#include "..\TestComObject1\TestComObject1.h"
07. 
08.DWORD WINAPI ThreadProc(LPVOID lpv)
09.{
10. 
11.HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
12. 
13.if ( FAILED(hr) )
14.{
15.std::cout << "CoinitializeEx failed!" << std::endl;
16.return 0;
17.}
18. 
19.ITestInterface1 *pTest = NULL;
20. 
21.hr = ::CoCreateInstance(CLSID_TestInterface1,
22.0,
23.CLSCTX_INPROC,
24.IID_ITestInterface1,
25.(void**)&pTest);
26. 
27.if ( FAILED(hr) )
28.{
29.std::cout << "CoCreateInstance failed!" << std::endl;
30.return 0;
31.}
32. 
33.hr = pTest->TestFunc1();
34. 
35.if ( FAILED(hr) )
36.{
37.std::cout << "TestFunc1 failed!" << std::endl;
38.return 0;
39.}
40. 
41.pTest->Release();
42.::CoUninitialize();
43.return 0;
44.}
45. 
46.int main(int argc, char* argv[])
47.{
48.HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
49. 
50.if ( FAILED(hr) )
51.{
52.std::cout << "CoinitializeEx failed!" << std::endl;
53.return 0;
54.}
55. 
56.ITestInterface1 *pTest = NULL;
57. 
58.hr = ::CoCreateInstance(CLSID_TestInterface1,
59.0,
60.CLSCTX_INPROC,
61.IID_ITestInterface1,
62.(void**)&pTest);
63. 
64.if ( FAILED(hr) )
65.{
66.std::cout << "CoCreateInstance failed!" << std::endl;
67.return 0;
68.}
69. 
70.hr = pTest->TestFunc1();
71. 
72.if ( FAILED(hr) )
73.{
74.std::cout << "TestFunc1 failed!" << std::endl;
75.return 0;
76.}
77. 
78.DWORD threadID;
79.HANDLE hThreads[1];
80.hThreads[0]  =   ::CreateThread(NULL,   //創建一個進程
81.0,
82.ThreadProc,
83.(LPVOID)pTest,  //將pTest作為一個參數傳入新線程
84.0,
85.&threadID);
86. 
87.::WaitForSingleObject(hThreads,INFINITE);   //等待線程結束
88.::CloseHandle(hThreads);                //關閉線程句柄
89.pTest->Release();
90.::CoUninitialize();
91.system("pause");
92.return 0;
93.}

以下是運行結果: 

    

可以看到,在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。好 的,我們需要重新寫一個函數,專門用來實現消息同步。

01.DWORD ApartMentMsgWaitForMultipleObject(HANDLE *hHandle,DWORD dwWaitCout, DWORD dwMilliseconds)
02.{
03.BOOL bQuit = FALSE;
04.DWORD dwRet;
05. 
06.while(!bQuit)
07.{
08.int rc;
09.rc = ::MsgWaitForMultipleObjects
10.  (
11.dwWaitCout, // 需要等待的對象數量
12.hHandle,    // 對象樹組
13.FALSE,      //等待所有的對象
14.(DWORD)dwMilliseconds,  // 等待的時間
15.(DWORD)(QS_ALLINPUT | QS_ALLPOSTMESSAGE)  // 事件類型   
16.  );
17.//等待的事件激發
18.if( rc ==  WAIT_OBJECT_0 )
19.{          
20.dwRet = rc;
21.bQuit = TRUE;
22.}
23.//其他windows消息
24.else if( rc == WAIT_OBJECT_0 + dwWaitCout )        
25.{
26.MSG msg;
27.while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
28.{
29.TranslateMessage (&msg);
30.DispatchMessage(&msg);
31.}          
32.}
33.}
34.return dwRet;
35.}

該函數用來處理消息的同步,也夠麻煩的,還需要自己寫這段程序。這段程序的意思是如果等待的事件被激發,那么設置bQuit為TURE,那么退出消息循環。如果接收到其它的消息的話,再分發出去。好了,把我們的程序再改一下:

1.//  ::WaitForSingleObject(hThreads,INFINITE);   //等待線程結束
2.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下可以找到。

001.#define _WIN32_WINNT 0x0400
002.#include < windows.h >
003.#include < iostream >
004. 
005.#include "..\TestComObject1\TestComObject1_i.c"
006.#include "..\TestComObject1\TestComObject1.h"
007. 
008.DWORD WINAPI ThreadProc(LPVOID lpv)
009.{
010.//HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
011.HRESULT hr = CoInitializeEx(NULL, COINIT_MULTITHREADED);
012. 
013.if ( FAILED(hr) )
014.{
015.std::cout << "CoinitializeEx failed!" << std::endl;
016.return 0;
017.}
018. 
019.IStream *pStream = (IStream*)lpv;
020. 
021.ITestInterface1 *pTest = NULL;
022. 
023.hr = ::CoGetInterfaceAndReleaseStream(pStream,
024.IID_ITestInterface1,
025.(void**)&pTest);
026.if ( FAILED(hr) )
027.{
028.std::cout << "CoGetInterfaceAndReleaseStream failed!" << std::endl;
029.return 0;
030.}
031. 
032. 
033.hr = pTest->TestFunc1();
034. 
035.if ( FAILED(hr) )
036.{
037.std::cout << "TestFunc1 failed!" << std::endl;
038.return 0;
039.}
040. 
041.pTest->Release();
042.::CoUninitialize();
043.return 0;
044.}
045. 
046.DWORD ApartMentMsgWaitForMultipleObject(HANDLE *hHandle,DWORD dwWaitCout, DWORD dwMilliseconds)
047.{
048. 
049.BOOL bQuit = FALSE;
050.DWORD dwRet;
051. 
052.while(!bQuit)
053.{
054.int rc;
055.rc = ::MsgWaitForMultipleObjects
056.(
057.dwWaitCout,    // 需要等待的對象數量
058.hHandle,            // 對象樹組
059.FALSE,              //等待所有的對象
060.(DWORD)dwMilliseconds,  // 等待的時間
061.(DWORD)(QS_ALLINPUT | QS_ALLPOSTMESSAGE)  // 事件類型   
062.);
063. 
064.if( rc ==  WAIT_OBJECT_0 )
065.{          
066.dwRet = rc;
067.bQuit = TRUE;
068. 
069.}
070.else if( rc == WAIT_OBJECT_0 + dwWaitCout )        
071.{
072.MSG msg;
073.while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
074.{
075.  TranslateMessage (&msg);
076.  DispatchMessage(&msg);
077.}          
078.}
079.}
080.return dwRet;
081.}
082. 
083.int main(int argc, char* argv[])
084.{
085.//HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
086.HRESULT hr = CoInitializeEx(NULL, COINIT_MULTITHREADED);
087. 
088.if ( FAILED(hr) )
089.{
090.std::cout << "CoinitializeEx failed!" << std::endl;
091.return 0;
092.}
093. 
094.ITestInterface1 *pTest = NULL;
095. 
096.hr = ::CoCreateInstance(CLSID_TestInterface1,
097.0,
098.CLSCTX_INPROC,
099.IID_ITestInterface1,
100.(void**)&pTest);
101. 
102.if ( FAILED(hr) )
103.{
104.std::cout << "CoCreateInstance failed!" << std::endl;
105.return 0;
106.}
107. 
108.hr = pTest->TestFunc1();
109. 
110.if ( FAILED(hr) )
111.{
112.std::cout << "TestFunc1 failed!" << std::endl;
113.return 0;
114.}
115. 
116.IStream *pStream = NULL;
117. 
118.hr = ::CoMarshalInterThreadInterfaceInStream(IID_ITestInterface1,
119.pTest,
120.&pStream);
121. 
122.if ( FAILED(hr) )
123.{
124.std::cout << "CoMarshalInterThreadInterfaceInStream failed!" << std::endl;
125.return 0;
126.}
127. 
128. 
129.DWORD threadID;
130.HANDLE hThreads[1];
131.hThreads[0]  =   ::CreateThread(NULL,           //創建一個進程
132.    0,
133.    ThreadProc,
134.    (LPVOID)pStream,  //將pStream作為一個參數傳入新線程
135.    0,
136.    &threadID);
137.ApartMentMsgWaitForMultipleObject(hThreads,1,INFINITE);
138.::CloseHandle(hThreads);                //關閉線程句柄
139.pTest->Release();
140.::CoUninitialize();
141.system("pause");
142.return 0;
143.}

我們通過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++文件中的實現代碼如下。

001.#define _WIN32_WINNT 0x0400
002.#include < windows.h >
003.#include < iostream >
004. 
005.#include "..\TestComObject1\TestComObject1_i.c"
006.#include "..\TestComObject1\TestComObject1.h"
007. 
008.DWORD WINAPI ThreadProc(LPVOID lpv)
009.{
010.HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
011.//HRESULT hr = CoInitializeEx(NULL, COINIT_MULTITHREADED);
012. 
013.if ( FAILED(hr) )
014.{
015.std::cout << "CoinitializeEx failed!" << std::endl;
016.return 0;
017.}
018. 
019.IStream *pStream = (IStream*)lpv;
020.ITestInterface1 *pTest = NULL;
021.hr = ::CoGetInterfaceAndReleaseStream(pStream,
022.IID_ITestInterface1,
023.(void**)&pTest);
024.if ( FAILED(hr) )
025.{
026.std::cout << "CoGetInterfaceAndReleaseStream failed!" << std::endl;
027.return 0;
028.}
029. 
030.std::cout << "ThradProc''s threadid is " << ::GetCurrentThreadId() << std::endl; //輸出ThradProc的線程ID
031. 
032. 
033.hr = pTest->TestFunc1();
034. 
035.if ( FAILED(hr) )
036.{
037.std::cout << "TestFunc1 failed!" << std::endl;
038.return 0;
039.}
040. 
041.pTest->Release();
042.::CoUninitialize();
043.return 0;
044.}
045. 
046.DWORD ApartMentMsgWaitForMultipleObject(HANDLE *hHandle,DWORD dwWaitCout, DWORD dwMilliseconds)
047.{
048. 
049.BOOL bQuit = FALSE;
050.DWORD dwRet;
051. 
052.while(!bQuit)
053.{
054.int rc;
055.rc = ::MsgWaitForMultipleObjects
056.(
057.dwWaitCout,    // 需要等待的對象數量
058.hHandle,            // 對象樹組
059.FALSE,              //等待所有的對象
060.(DWORD)dwMilliseconds,  // 等待的時間
061.(DWORD)(QS_ALLINPUT | QS_ALLPOSTMESSAGE)  // 事件類型   
062.);
063. 
064.if( rc ==  WAIT_OBJECT_0 )
065.{          
066.dwRet = rc;
067.bQuit = TRUE;
068. 
069.}
070.else if( rc == WAIT_OBJECT_0 + dwWaitCout )        
071.{
072.MSG msg;
073. 
074.while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
075.{
076.  TranslateMessage (&msg);
077.  DispatchMessage(&msg);
078.}          
079.}
080.}
081. 
082.return dwRet;
083.}
084. 
085.int main(int argc, char* argv[])
086.{
087.HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
088.//HRESULT hr = CoInitializeEx(NULL, COINIT_MULTITHREADED);
089. 
090.if ( FAILED(hr) )
091.{
092.std::cout << "CoinitializeEx failed!" << std::endl;
093.return 0;
094.}
095. 
096.ITestInterface1 *pTest = NULL;
097. 
098.hr = ::CoCreateInstance(CLSID_TestInterface1,
099.0,
100.CLSCTX_INPROC,
101.IID_ITestInterface1,
102.(void**)&pTest);
103. 
104.if ( FAILED(hr) )
105.{
106.std::cout << "CoCreateInstance failed!" << std::endl;
107.return 0;
108.}
109. 
110.std::cout << "main''s threadid is " << ::GetCurrentThreadId() << std::endl;  //打印main的線程ID
111. 
112.hr = pTest->TestFunc1();
113. 
114.if ( FAILED(hr) )
115.{
116.std::cout << "TestFunc1 failed!" << std::endl;
117.return 0;
118.}
119. 
120.IStream *pStream = NULL;
121. 
122.hr = ::CoMarshalInterThreadInterfaceInStream(IID_ITestInterface1,
123.pTest,
124.&pStream);
125. 
126.if ( FAILED(hr) )
127.{
128.std::cout << "CoMarshalInterThreadInterfaceInStream failed!" << std::endl;
129.return 0;
130.}
131. 
132. 
133.DWORD threadID;
134.HANDLE hThreads[1];
135.hThreads[0] =   ::CreateThread(NULL,            //創建一個進程
136.    0,
137.    ThreadProc,
138.    (LPVOID)pStream,  //將pStream作為一個參數傳入新線程
139.    0,
140.    &threadID);
141. 
142.ApartMentMsgWaitForMultipleObject(hThreads,1,INFINITE);
143.::CloseHandle(hThreads);                //關閉線程句柄
144.pTest->Release();
145.::CoUninitialize();
146.system("pause");
147.return 0;
148.}

此部分代碼與我們測試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進行改造。

01.class ATL_NO_VTABLE CTestInterface1 :
02.public CComObjectRootEx,
03.public CComCoClass,
04.public IDispatchImpl
05.{
06.private:
07.DWORD dwTlsIndex;
08.public:
09.CTestInterface1()
10.{
11.dwTlsIndex = TlsAlloc();
12.HLOCAL l =  LocalAlloc(LMEM_FIXED, 1);
13.TlsSetValue(dwTlsIndex, l);   
14.}

我們先聲明一個私有成員變量dwTlsIndex,它用來存放TLS的索引值(一個線程的TLS相當于一個數組,可以存放不同的數據)。再將構造函數中填 入保存數據的代碼。此處只是簡單的分配了一個字節的地址,并將該地址通過TlsSetValue保存到TLS中去。

然后再改造我們的TestFunc1函數。如下:

01.STDMETHODIMP CTestInterface1::TestFunc1()
02.{
03.// TODO: Add your implementation code here
04.LPVOID lpvData = TlsGetValue(dwTlsIndex);
05.if ( lpvData == NULL )
06.return RPC_E_WRONG_THREAD;
07. 
08.std::cout << "In the itestinferface1''s object, the thread''s id is " << ::GetCurrentThreadId() << std::endl;
09.return S_OK;
10.}

這邊也很簡單,就是簡單的通過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的將來會怎么樣,但我覺得理解一個東西總是有樂趣的。與你同勉。