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


簡(jiǎn)序

大學(xué)畢業(yè)前的最后一學(xué)期,在一家公司實(shí)習(xí),當(dāng)時(shí)的工作需要用到一些操作系統(tǒng)提供的組件。那時(shí)候只知道COM這個(gè)名詞,并不知道到底是怎么回事,只知道上網(wǎng) 到處找別人的源碼解決自己的問題;那段日子到現(xiàn)在回憶起來都是灰色的,每天呆坐在電腦前,一個(gè)網(wǎng)站一個(gè)網(wǎng)站的查找自己需要的源碼。但并不清楚自己到底在做 什么;那時(shí)候?qū)ψ约耗懿荒艹蔀橐粋€(gè)程序員充滿了懷疑。在實(shí)習(xí)結(jié)束返校的火車上,一夜間,我把一本《COM本質(zhì)論》翻看了120多頁。當(dāng)我和當(dāng)時(shí)的女友吹噓 自己一夜可以看100多頁書的時(shí)候,她馬上問我:看懂多少?當(dāng)時(shí)我啞口無言。她忍受不了我那段日子的失落和抱怨,從那時(shí)候起,我們結(jié)束了那段簡(jiǎn)短的感情。 到如今我還在一個(gè)人漂泊著,而上周她成為了別人的妻子。想不到用什么方式去紀(jì)念我迄今為止經(jīng)歷過的唯一一段感情,我和她的感情并不完全是因?yàn)镃OM結(jié)束 的,但由于對(duì)COM的迷惑,使我走向了迷茫,失落;對(duì)自己失去了信心,在她面前變成了一個(gè)悲觀失望的人。寫這篇文章權(quán)當(dāng)對(duì)這份感情的一份紀(jì)念吧。

企者不立,跨著不行。很多格言都告訴我們做什么事情都必須從基礎(chǔ)開始,對(duì)COM的理解也是這個(gè)道理。當(dāng)三年前我看《COM 本質(zhì)論》的時(shí)候,對(duì)虛函數(shù)也只是一知半解,只是知道通過它可以實(shí)現(xiàn)多態(tài)。但到底怎么實(shí)現(xiàn)就不清楚了。看不懂COM太正常了。知道看過Stanley B.Lippman的《Inside the C++ Object Model》,對(duì)C++的內(nèi)存結(jié)構(gòu)有了基本的理解,我才明白了接口的意義。這篇文章是寫給初學(xué)者的,順便給大家一些建議,如果一本書你看不懂的時(shí)候,可以 先放放,先找一些基礎(chǔ)的讀物來看看。這樣可以少走一些彎路。

Don Box 在《COM 本質(zhì)論》中說,對(duì)接口,類對(duì)象和套間有了徹底的理解,那么使用COM,沒有翻不過去的山頭。如果你對(duì)C++有深入的理解,那么《COM本質(zhì)論》中對(duì)接口和 類對(duì)象的闡述很清晰,理解并不困難。但套間是一個(gè)比較抽象的概念,而書上對(duì)這部分只是理論的敘述,沒有提供具體的例子,理解起來就更困難了。在此我把自己 找到的一些例子和自己的理解總結(jié)以下,以期給初學(xué)者提供一些入門的方法。閑話打住,開始正文吧。

一、關(guān)于多線程(Multithreading)

子曰:本立道生。也就是說我們明白事物所存在的原因,自然也就明白事物是怎么回事了。如果我們清楚了套間(Apartment)的產(chǎn)生原因,再去理解套 間,就容易許多了。我們先來看看,為什么需要套間?套間是為解決多線程中使用組件而產(chǎn)生的,首先我們來了解一下多線程。 

1、理解進(jìn)程(Processes)和線程(Threading)

理解線程,先從進(jìn)程(Processes)開始,一般書上對(duì)進(jìn)程的描述都比較抽象,都說進(jìn)程是一個(gè)運(yùn)行的程序的實(shí)例,進(jìn)程擁有內(nèi)存,資源。我這兒試著用一 段匯編程序來解釋一下進(jìn)程,看看能不能幫你加深一下印象。我們先來看一段簡(jiǎn)單的匯編程序(你不理解匯編的話,建議找本書看看,一點(diǎn)不懂匯編,很難對(duì)其它高 級(jí)語言有太深的理解)。

01.; 匯編程序示例
02.data_seg segment  ;定義數(shù)據(jù)段
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  ;保存舊的數(shù)據(jù)段
20.sub ax,ax
21.push ax
22. 
23.mov ax,data_seg     ;將定義的數(shù)據(jù)段保存到ds
24.mov ds,ax
25. 
26.call fact               ;調(diào)用子函數(shù)
27. 
28.…….             ;其它操作省略
29.ret     ;返回到系統(tǒng)
30.main endp
31. 
32.fact proc near       ;子函數(shù)定義
33. 
34.……              ;具體操作省略
35.ret  ;返回到調(diào)用處
36.fact endp
37. 
38.code1 ends
39.end start
40.示例1:匯編程序結(jié)構(gòu)

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

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

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

假如我們的程序運(yùn)行到mov eax,4,那么EIP就會(huì)指向該句代碼所在的內(nèi)存的地址。當(dāng)這行代碼執(zhí)行完畢之后,那么EIP會(huì)自動(dòng)加一,那么它就會(huì)指向mov ebx,4。而程序的執(zhí)行就是靠EIP的不斷增加來完成的(跳轉(zhuǎn)的話,EIP就變成了跳轉(zhuǎn)到的地址)。在Windows系統(tǒng)下,進(jìn)程并不擁有 EIP,EAX,那么只有進(jìn)程,一個(gè)程序就無法運(yùn)行。而擁有這些寄存器的是線程,所以說進(jìn)程是靜態(tài)的。

我們知道一個(gè)CPU下只有一個(gè)EIP,一個(gè)EAX,也就是說同一時(shí)刻只能有一個(gè)線程可以運(yùn)行,那么所說的多線程又是什么呢?事實(shí)上同一時(shí)刻也只有一個(gè)線程 在運(yùn)行,每個(gè)線程運(yùn)行一段時(shí)間后,它會(huì)把它擁有的EIP,EAX等寄存器讓出來,其它線程占有這些寄存器后,繼續(xù)運(yùn)行。因?yàn)檫@段時(shí)間很短,所以我們感覺不 出來。這樣我們就可以在一邊聽音樂的時(shí)候,一邊玩俄羅斯方塊了。為了實(shí)現(xiàn)不同的線程之間的轉(zhuǎn)換,CPU要求操作系統(tǒng)維護(hù)一份固定格式的數(shù)據(jù)(該數(shù)據(jù)存在于 內(nèi)存中),這份數(shù)據(jù)叫做Task-State Segment(TSS),在這份數(shù)據(jù)結(jié)構(gòu)里,維護(hù)著線程的EAX,EIP,DS等寄存器的內(nèi)容。而CPU還有一個(gè)寄存器叫做Task Register(TR),該寄存器指向當(dāng)前正在執(zhí)行的線程的TSS。而線程切換事實(shí)上就是TR指向不同的TSS,這樣CPU就會(huì)自動(dòng)保存當(dāng)前的 EAX,EBX的信息到相應(yīng)的TSS中,并將新的線程的信息加載到寄存器。

事實(shí)上線程不過上一些數(shù)據(jù)結(jié)構(gòu),這些結(jié)構(gòu)保存了程序執(zhí)行時(shí)候需要的一些信息。我們可以在windows提供的頭文件中找到一些影子,安裝VC后在它的 include目錄下有一個(gè)Winnt.h文件。在該文件中,我們可以找到這樣一個(gè)struct(_CONTEXT)。這就是線程切換時(shí)需要的數(shù)據(jù)結(jié)構(gòu) (我不確定Windows內(nèi)部是否用的就是這個(gè)結(jié)構(gòu),但應(yīng)該和這份數(shù)據(jù)相差無幾)。

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;

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

2、多線程存在的問題

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

01.#include < iostream >
02.#include < windows.h >
03. 
04.int g_i = 10;  //一個(gè)全局變量
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++ )         //創(chuàng)建兩個(gè)線程
23.hThreads[i] = ::CreateThread(NULL,
24.        0,
25.        ThreadProc,
26.        NULL,
27.        0,
28.        &threadID[i]);
29. 
30. 
31.WaitForMultipleObjects(2,hThreads,TRUE,INFINITE);   //等待線程結(jié)束
32. 
33.for(i = 0; i <= 1; i++ )
34.::CloseHandle(hThreads[i]);             //關(guān)閉線程句柄
35.system("pause");
36.return 0;
37.}
38.示例程序2-多線程程序

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

 

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

01.#include < iostream >
02.#include < windows.h >
03. 
04.int g_i = 10;  //一個(gè)全局變量
05. 
06.CRITICAL_SECTION cs;  //一個(gè)臨界區(qū)變量
07. 
08.DWORD WINAPI ThreadProc(LPVOID lpv)
09.{
10.EnterCriticalSection(&cs);  //進(jìn)入臨界區(qū)
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++ )            //創(chuàng)建兩個(gè)線程
30.hThreads[i] = ::CreateThread(NULL,
31.    0,
32.    ThreadProc,
33.    NULL,
34.    0,
35.    &threadID[i]);
36. 
37.WaitForMultipleObjects(2,hThreads,TRUE,INFINITE);   //等待線程結(jié)束
38.for(i = 0; i < = 1; i++ )
39.::CloseHandle(hThreads[i]);             //關(guān)閉線程句柄
40. 
41.system("pause");
42.return 0;
43.}

再次運(yùn)行,結(jié)果就是我們所需要的了。 

     

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

注意:以上只是一個(gè)簡(jiǎn)單的例子,事實(shí)上多線程中需要保護(hù)的部分一般集中在全局?jǐn)?shù)據(jù)和靜態(tài)數(shù)據(jù)之上,因?yàn)檫@樣的數(shù)據(jù)每個(gè)進(jìn)程只有一份,如上所示的g_i。 (想對(duì)多線程程序有更深入的認(rèn)識(shí),可以找侯捷翻譯的《Win32多線程程序設(shè)計(jì)》看看,90年代出的書,到現(xiàn)在還暢銷,足可以說明它的價(jià)值)

二、套間所要解決的問題   

從多線程的描述中,我們知道,套間所要解決的問題是幫助組件的開發(fā)者在實(shí)現(xiàn)多線程下調(diào)用組件時(shí)候的同步問題。我們還是先看一段簡(jiǎn)短的程序。

我們首先使用ATL創(chuàng)建一個(gè)簡(jiǎn)單的組件程序,該程序有一個(gè)接口(ITestInterface1),該接口支持一個(gè)方法TestFunc1。(該組件可以 在附加的源碼的“Apartment\TestComObject1”目錄下找到)我們通過以下的程序調(diào)用該組件。(該程序可以在附加的源碼的 “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, //創(chuàng)建一個(gè)進(jìn)程
72.0,
73.ThreadProc,
74.NULL,  //將pTest作為一個(gè)參數(shù)傳入新線程
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);   //等待線程結(jié)束
86.::CloseHandle(hThreads);                //關(guān)閉線程句柄
87.pTest->Release();
88.::CoUninitialize();
89.system("pause");
90.return 0;
91.}

該段程序?qū)ain中定義的ITestInterface1對(duì)象,通過指針傳到了新建的線程中。運(yùn)行該段程序,結(jié)果如下,又是一串亂序的文字串。也就是說 我們需要在TestComObject1中對(duì)TestFunc1進(jìn)行線程同步控制。但大多數(shù)人并不想這樣做,因?yàn)槲覀冮_發(fā)的組件大多數(shù)情況下并不會(huì)在多線 程執(zhí)行。但為了避免低概率事件發(fā)生后的不良后果,套間出場(chǎng)了。 

 

三、套間如何實(shí)現(xiàn)數(shù)據(jù)的同步

我們已經(jīng)知道套間的目的是用來實(shí)現(xiàn)數(shù)據(jù)的同步,那么套間如何來實(shí)現(xiàn)呢?如果我們能保證COM對(duì)象中的函數(shù)只能在該對(duì)象中的另一個(gè)函數(shù)執(zhí)行完以后,才能開始 執(zhí)行(也就是說組件中的函數(shù)只能一個(gè)一個(gè)的執(zhí)行),那么我們的問題就可以解決了。是的,你可以發(fā)現(xiàn),這樣的話,就失去了多線程的優(yōu)勢(shì);但套間的目的是保證 小概率下的線程安全,損耗一些性能,應(yīng)該比出現(xiàn)邏輯錯(cuò)誤強(qiáng)點(diǎn)。 

那么又如何保證同一對(duì)象下的所有方法都必須按順序逐個(gè)執(zhí)行呢?微軟的COM API設(shè)計(jì)者們借用了Windows的消息機(jī)制。我們先來看一下windows的消息機(jī)制圖。 

     

我們可以看到所有線程發(fā)出的消息都回首先放到消息隊(duì)列中,然后在通過消息循環(huán)分發(fā)到各自窗口去,而消息隊(duì)列中的消息只能一個(gè)處理完后再處理另一個(gè),借助消 息機(jī)制,就可以實(shí)現(xiàn)COM的函數(shù)一個(gè)一個(gè)的執(zhí)行,而不會(huì)同時(shí)運(yùn)行。Windows的消息機(jī)制是通過窗口來實(shí)現(xiàn)的,那么一個(gè)線程要接收消息,也應(yīng)該有一個(gè)窗 口。 COM API的設(shè)計(jì)者在它們的API函數(shù)中實(shí)現(xiàn)了一個(gè)隱藏的窗口。在我們調(diào)用CoInitializeEx(NULL, COINIT_APARTMENTTHREADED)的時(shí)候,會(huì)生成這個(gè)窗口。(如果你對(duì)softice等動(dòng)態(tài)調(diào)試工具熟悉的話,可以通過跟蹤源碼來跟蹤 CoInitializeEx函數(shù),可以發(fā)現(xiàn)它會(huì)調(diào)用API函數(shù)CreateWindowEx)。該窗口是隱藏的,有了這個(gè)窗口,就可以支持消息機(jī)制,就 有辦法來實(shí)現(xiàn)對(duì)象中函數(shù)的逐一執(zhí)行。這樣當(dāng)對(duì)象指針被傳到其它線程的時(shí)候,從外部調(diào)用該對(duì)象的方法的時(shí)候,就會(huì)先發(fā)一個(gè)消息到原線程,而不再直接訪問對(duì)象 了。套間的原理大致就是這樣。我們?cè)賮砜纯碈OM中的套間類型。

四、套間的類型

     

我們首先看看ATL為我們提供的線程類型:Single,Apartment,Both,F(xiàn)ree。我們還是通過例子來說明它們的不同。我們?nèi)匀挥梦覀兪褂脛偛艑?shí)現(xiàn)的TestComObject1來進(jìn)行測(cè)試,先對(duì)它實(shí)現(xiàn)的唯一方法進(jìn)行一下說明。

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.}

該方法非常簡(jiǎn)單,就是打印出該方法運(yùn)行時(shí),所在的線程的ID號(hào)。如果在不同的線程中調(diào)用同一個(gè)對(duì)象的時(shí)候,通過套間,發(fā)送消息,最終該對(duì)象只應(yīng)該在一個(gè)線程中運(yùn)行,所以它的線程ID號(hào)應(yīng)該是相同的。我們將通過該ID值來驗(yàn)證套間的存在。

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,   //創(chuàng)建一個(gè)進(jìn)程
81.0,
82.ThreadProc,
83.(LPVOID)pTest,  //將pTest作為一個(gè)參數(shù)傳入新線程
84.0,
85.&threadID);
86. 
87.::WaitForSingleObject(hThreads,INFINITE);   //等待線程結(jié)束
88.::CloseHandle(hThreads);                //關(guān)閉線程句柄
89.pTest->Release();
90.::CoUninitialize();
91.system("pause");
92.return 0;
93.}

以下是運(yùn)行結(jié)果: 

    

可以看到,在main中我們創(chuàng)建了一個(gè)ITestInterface1接口對(duì)象,并調(diào)用TestFunc1,此處會(huì)輸出一個(gè)線程 ID――ThreadID1。之后主線程生成一個(gè)線程,在該線程中,我們會(huì)再次生成一個(gè)ITestInterface1接口對(duì)象,此處再次調(diào)用 TestFunc1,可以看到輸出了另一個(gè)線程ID――ThreadID2。因?yàn)槭遣煌膶?duì)象,所以它們的線程ID號(hào)不同。(注意了,此處并沒有跨線程調(diào) 用對(duì)象,并不在套間的保護(hù)范圍)

好了,我們?cè)搧砜纯碨ingle類型的套間了。如果你和我一樣懶,不想為此去寫一個(gè)single類型的接口,那么打開你的注冊(cè)表。

    

找到我們的接口ID,在InprocServer32項(xiàng)下,將ThreadingModel的值改為Single,或者將該項(xiàng)刪除(這樣也代表是Single套間)。我們?cè)賮磉\(yùn)行該程序,再看運(yùn)行結(jié)果。 

 

當(dāng)打印出一個(gè)線程ID的時(shí)候,程序就停止了。Why?剛開始,我也被搞的頭暈?zāi)X脹。到MSDN中查找WaitForSingleObject,原來 WaitForSingleObject會(huì)破壞程序中的消息機(jī)制,這樣在創(chuàng)建的線程中,TestFunc1需要通過消息機(jī)制來運(yùn)行,消息機(jī)制破壞,就無法 運(yùn)行了。哎!還的再改程序。在查查《Win32多線程程序設(shè)計(jì)》,原來在GUI中等待線程需要用MsgWaitForMultipleObjects。好 的,我們需要重新寫一個(gè)函數(shù),專門用來實(shí)現(xiàn)消息同步。

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, // 需要等待的對(duì)象數(shù)量
12.hHandle,    // 對(duì)象樹組
13.FALSE,      //等待所有的對(duì)象
14.(DWORD)dwMilliseconds,  // 等待的時(shí)間
15.(DWORD)(QS_ALLINPUT | QS_ALLPOSTMESSAGE)  // 事件類型   
16.  );
17.//等待的事件激發(fā)
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.}

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

1.//  ::WaitForSingleObject(hThreads,INFINITE);   //等待線程結(jié)束
2.ApartMentMsgWaitForMultipleObject(hThreads,1,INFINITE);

我們?cè)賮砜匆幌逻\(yùn)行結(jié)果。 

    

我們可以看到兩處調(diào)用TestFunc1,得到的線程ID是相同的。我們?cè)偻ㄟ^VC的調(diào)試功能來看看第二個(gè)TestFunc1的運(yùn)行過程。我們?cè)趦蓚€(gè) TesfFunc1調(diào)用處設(shè)置斷點(diǎn),然后通過F11跟蹤進(jìn)TestFunc1來看看它的調(diào)用過程。以下是在Main中的調(diào)用過程。

  

通過Call Stack,我們可以看到,此處是在main中直接調(diào)用的。我們?cè)賮砜吹诙幷{(diào)用:

  

我們可以看到TestFunc1的調(diào)用需要通過一連串的API方法來實(shí)現(xiàn)。你感興趣的話,可以通過反匯編的方法來跟蹤一下這些API,看看它們具體實(shí)現(xiàn)了 什么,這里我們可以看到這些函數(shù)在dll中的大致位置,你可以使用W32DASM等反匯編工具打開這些dll,大致研究一下這些函數(shù)。

好了,我們已經(jīng)看到了Single套間的作用。那么Single套間究竟是什么意思呢?就是說每個(gè)被標(biāo)志為Single的接口,在一個(gè)進(jìn)程中只會(huì)存活在一 個(gè)套間中。該套間就是進(jìn)程創(chuàng)建的第一個(gè)套間。你可以將Main中與pTest相關(guān)的代碼都去掉,只保留CoInitializeEx(NULL, COINIT_APARTMENTTHREADED)和線程的創(chuàng)建,再次運(yùn)行該程序,可以發(fā)現(xiàn)創(chuàng)建線程中的TestFunc1仍然是通過消息來實(shí)現(xiàn)的。

好了看過了Single,我們還是在注冊(cè)表中,將ThreadingModel改為Apartment。通過修改注冊(cè)表就可以實(shí)現(xiàn)對(duì)套間類型的控制,證明 了套間和我們的程序本身沒有什么關(guān)系,ATL的選項(xiàng)所做的作用也只是通過它來添加注冊(cè)表。套間只是對(duì)系統(tǒng)的一種提示,由COM API通過注冊(cè)表信息來幫我們實(shí)現(xiàn)套間。

2、Apartment

在第二部分(套間所要解決的問題),我們?cè)?jīng)提供了一個(gè)不同線程共享接口對(duì)象的方法,該方法是錯(cuò)誤的(我們也可以通過程序阻止這種用法,稍候再敘)。此處我們提供一種正確的做法。以下代碼在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,    // 需要等待的對(duì)象數(shù)量
058.hHandle,            // 對(duì)象樹組
059.FALSE,              //等待所有的對(duì)象
060.(DWORD)dwMilliseconds,  // 等待的時(shí)間
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,           //創(chuàng)建一個(gè)進(jìn)程
132.    0,
133.    ThreadProc,
134.    (LPVOID)pStream,  //將pStream作為一個(gè)參數(shù)傳入新線程
135.    0,
136.    &threadID);
137.ApartMentMsgWaitForMultipleObject(hThreads,1,INFINITE);
138.::CloseHandle(hThreads);                //關(guān)閉線程句柄
139.pTest->Release();
140.::CoUninitialize();
141.system("pause");
142.return 0;
143.}

我們通過CoGetInterfaceAndReleaseStream將main中的pTest變?yōu)閜Stream,然后將pStream作為參數(shù)傳入 到線程中,然后再通過CoGetInterfaceAndReleaseStream將pSteam變?yōu)榻涌谥羔槨T賮砜纯催\(yùn)行的結(jié)果:

  

可以看到兩次運(yùn)行,線程ID是相同的。好的,我們接著改變注冊(cè)表,再將Apartment變?yōu)镕ree。然后再將兩處的HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);改為HRESULT hr = CoInitializeEx(NULL, COINIT_MULTITHREADED)。編譯后再次執(zhí)行該程序,再來看執(zhí)行結(jié)果。

  

我們可以看到兩個(gè)線程的ID是不同的。你可以通過VC的Debug來看這兩組程序的TesFunc1的調(diào)用情況,在第二種情況下,創(chuàng)建的線程中不會(huì)通過消息機(jī)制來調(diào)用該函數(shù)。 

通過對(duì)比,我們可以知道所說的套間,就是通過消息機(jī)制來控制不同線程中對(duì)對(duì)象的調(diào)用。這樣就不需要組件的實(shí)現(xiàn)者來實(shí)現(xiàn)數(shù)據(jù)的同步。

3、Free

上節(jié)的例子,已經(jīng)為我們提示了我們Free套間,其實(shí)系統(tǒng)對(duì)我們的組件不做控制,這樣就需要組件的開發(fā)者對(duì)數(shù)據(jù)的同步做出控制。

4、Both

所謂Both,就是說該對(duì)象既可以運(yùn)行在Apartment中,也可以運(yùn)行在Free套間中。該類型的前提是它應(yīng)該是Free類型的套間,也就是說組件自己實(shí)現(xiàn)了數(shù)據(jù)的同步。然后設(shè)置成Both類型。 

為什么需要Both類型的套間呢?想想假如我們?cè)谖覀兊慕M件中調(diào)用另一個(gè)組件,這樣我們就需要在我們的組件中為所調(diào)用的組件來開辟一個(gè)套間。我們的套間是 一個(gè)Apartment,而調(diào)用的組件是Free類型的,這樣這兩個(gè)對(duì)象就必須存在于不同的兩個(gè)套間中。而跨套間的調(diào)用,需要通過中間代理來實(shí)現(xiàn),這樣必 然會(huì)損失性能。但如果我們調(diào)用的套間類型是Both的話,它就可以和我們的組件同享一個(gè)套間,這樣就可以提高效率。

五、缺省套間

繼續(xù)我們的測(cè)試,首先在注冊(cè)表中將我們的接口類型改回Apartment。然后新建一個(gè)工程DefaultApartment。C++文件中的實(shí)現(xiàn)代碼如下。

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,    // 需要等待的對(duì)象數(shù)量
058.hHandle,            // 對(duì)象樹組
059.FALSE,              //等待所有的對(duì)象
060.(DWORD)dwMilliseconds,  // 等待的時(shí)間
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,            //創(chuàng)建一個(gè)進(jìn)程
136.    0,
137.    ThreadProc,
138.    (LPVOID)pStream,  //將pStream作為一個(gè)參數(shù)傳入新線程
139.    0,
140.    &threadID);
141. 
142.ApartMentMsgWaitForMultipleObject(hThreads,1,INFINITE);
143.::CloseHandle(hThreads);                //關(guān)閉線程句柄
144.pTest->Release();
145.::CoUninitialize();
146.system("pause");
147.return 0;
148.}

此部分代碼與我們測(cè)試Apartment時(shí)的代碼基本相同,只是新增了輸出main和創(chuàng)建線程的ID的語句。好的,我們來運(yùn)行程序,可以得到如下的結(jié)果:

 

我們可以看到main的線程ID和兩個(gè)TestFunc1的線程ID相同。也就是說兩個(gè)TestFunc1都是在main的線程中運(yùn)行的。 

將我們的程序做些變動(dòng),將CoInitializeEx(NULL, COINIT_APARTMENTTHREADED)改為 CoInitializeEx(NULL, COINIT_MULTITHREADED)。然后接著運(yùn)行程序。我們?cè)賮砜催\(yùn)行的結(jié)果。

 

我們可以看到兩個(gè)TestFunc1的線程ID和main的不同了,和我們創(chuàng)建的線程也不同。這是為什么呢?CoInitializeEx是一個(gè)創(chuàng)建套間 的過程,我們使用CoInitializeEx(NULL, COINIT_MULTITHREADED)后,沒有為我們的組件創(chuàng)建合適的套間。這時(shí)候系統(tǒng)(也就是COM API,這里應(yīng)該是通過CoCreateInstance來實(shí)現(xiàn)的)就會(huì)幫我們將我們的接口對(duì)象放入缺省套間,該套間并不運(yùn)行在當(dāng)前的線程中。我們?cè)俅卧?Debug下跟蹤運(yùn)行過程,可以發(fā)現(xiàn)在main中調(diào)用TestFunc1,也需要通過眾多的API函數(shù)幫助完成,也就是說此處也是通過消息機(jī)制來完成的, 這樣性能上肯定會(huì)有影響。

六、阻止接口指針的非法使用

在第二部分我們給出了一個(gè)通過直接傳輸接口指針到另外線程的例子,事實(shí)上這種方法是錯(cuò)誤的,但COM API并沒有幫助我們阻止這樣的錯(cuò)誤。這個(gè)任務(wù)可以由我們自己來完成。

因?yàn)樘组g是和線程相關(guān)的,Apartment類型的接口方法只應(yīng)該運(yùn)行在一個(gè)套間中(其實(shí)這就是一個(gè)協(xié)議,并不是強(qiáng)制性的),那么我們可以通過線程的相關(guān)性質(zhì)來實(shí)現(xiàn)。

在線程中我們可以通過Thread Local Storage(TLS)來保存線程的相關(guān)信息,同一函數(shù)運(yùn)行在不同的線程中,那么它所擁有的TLS也不相同。

我們來動(dòng)手改造我們的類實(shí)現(xiàn),將CTestferface1進(jìn)行改造。

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.}

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

然后再改造我們的TestFunc1函數(shù)。如下:

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.}

這邊也很簡(jiǎn)單,就是簡(jiǎn)單的通過TlsGetValue去嘗試得到dwTlsIndex所標(biāo)志的內(nèi)容是否存在。如果不存在,那么就說明程序運(yùn)行在了不同的套 間中。就會(huì)返回RPC_E_WRONG_THREAD,這是COM設(shè)計(jì)者定義的宏,表示線程的非法使用。(由于我的懶惰,不再寫新的COM了,只是簡(jiǎn)單的 修改了TestComObject1,這部分新加的代碼被我注釋掉了,你如果想看這部分的效果,去掉注釋就可以了)

我們?cè)龠\(yùn)行ErrorUseApartment程序,發(fā)現(xiàn)TestFunc1已經(jīng)無法輸出線程號(hào),而是直接返回RPC_E_WRONG_THREAD。再次運(yùn)行ApartmentTest程序,發(fā)現(xiàn)這樣的處理對(duì)它并沒有影響。仍然正常運(yùn)行。

六、什么是套間?

我們從外部表現(xiàn)上對(duì)套間進(jìn)行了了解,而套間究竟是什么?潘愛民譯的《Com 本質(zhì)論》說:套間既不是進(jìn)程,也不是線程,然而套間擁有進(jìn)程和線程的某些特性。我覺得,這句話翻譯的不到位,總讓人感覺套間似乎是和進(jìn)程或者線程等同的東 西。找來原文看看:An apartment is neither a process nor a thread; however, apartments share some of the properties of both。這里的share被譯成了擁有,但我感覺此處翻譯為使用或者分享可能更貼切一些。不過原文事實(shí)上也很容易給初學(xué)者帶來誤導(dǎo)。其實(shí)套間只是保存在 線程中的一個(gè)數(shù)據(jù)結(jié)構(gòu)(還有一個(gè)隱藏著的窗口),借用該結(jié)構(gòu)使套間和線程之間建立起某種關(guān)系,通過該關(guān)系,使得COM API通過該信息可以建立不同套間中的調(diào)用機(jī)制。這部分涉及到列集,散集(我們調(diào)用 CoMarshalInterThreadInterfaceInStream,CoGetInterfaceAndReleaseStream的過 程)。在列集和散集過程中,COM API會(huì)幫我們建立一個(gè)不同套間中對(duì)象通信機(jī)制,這部分涉及到了代理,存根和通道的內(nèi)容。通過代理來發(fā)送調(diào)用信息,通過通道發(fā)送到存根,再通過存根調(diào)用實(shí) 際的方法(其實(shí)那個(gè)隱藏的窗口就是為存根來服務(wù)的)。所做的這一切不過是為了實(shí)現(xiàn)不同套間中可以通過消息來調(diào)用對(duì)象。你可以找《Com 本質(zhì)論》來看看,這部分的內(nèi)容比較繁雜,但我感覺比起套間的概念,還是比較容易的。

具體實(shí)現(xiàn)套間,在線程的TLS究竟保存了什么信息呢?罪惡的微軟隱藏了這邊部分內(nèi)容,我們無法得到這部分的材料。這可能也是套間理解起來如此困難的一個(gè)原 因,套間呈現(xiàn)給我們的是一個(gè)抽象的概念。但理解其實(shí)際意義后,抽不抽象已經(jīng)沒什么關(guān)系,因?yàn)樗[藏的不過是創(chuàng)建和使用套間時(shí)候繁雜的調(diào)用其它API函數(shù) 的過程,事實(shí)上并沒有太多的神秘可言。對(duì)我們開發(fā)者來說,能明白套間的意義,已經(jīng)足夠了。

好了,稍微總結(jié)一下:套間是保存在線程的TLS中的一個(gè)數(shù)據(jù)結(jié)構(gòu),通過該結(jié)構(gòu)可以幫助不同的套間之間通過消息機(jī)制來實(shí)現(xiàn)函數(shù)的調(diào)用,以保證多線程環(huán)境下,數(shù)據(jù)的同步。

結(jié)語

石康說:比爾.蓋茨并不是什么天才,軟件工作者充其量不過是一個(gè)技術(shù)工作者,無法和科學(xué)工作者同日而語。石康還說:如果給他老人家足夠的時(shí)間,他也可以寫 出一個(gè)操作系統(tǒng)。呵呵,大意好象如此,似乎是他老人家在《支離破碎》中的名言,現(xiàn)在記不太清楚了。剛開始覺得他老人家太狂了,不過仔細(xì)體會(huì)一下,確實(shí)如 此。計(jì)算機(jī)的世界很少有真正高深的東西,有些內(nèi)容你不理解,肯定是你的某方面的基礎(chǔ)不扎實(shí)。不理解接口,那是因?yàn)槟愕腃++沒學(xué)好;不理解套間,那是因?yàn)?你不懂多線程;不懂多線程那是因?yàn)槟悴欢瓹PU的結(jié)構(gòu)。

技術(shù)革新在眼花繚亂的進(jìn)行的,.Net,Web services,到處閃現(xiàn)著新鮮的名詞,似乎這個(gè)世界每天都在變化的。但事實(shí)上,從286到386,從dos到圖形操作系統(tǒng)后,計(jì)算機(jī)再?zèng)]有什么重大的 革新。從我們開發(fā)者的角度來看,不過是開發(fā)工具的更新。但每次開發(fā)工具的更新都能使很多人興奮異常,激動(dòng)著下載安裝最新版本的工具,追逐著學(xué)習(xí)最新的開發(fā) 語言。總覺的這樣就不會(huì)被時(shí)代所拋棄,總以為開發(fā)工具會(huì)幫著提升自己的價(jià)值。事實(shí)上呢?學(xué)會(huì)拖拉創(chuàng)建窗口的人,可能根本不知道Windows中有一個(gè)消息 機(jī)制。開發(fā)十多年的人會(huì)把一個(gè)棧中生成的對(duì)象的地址作為參數(shù)傳給接收者。沒有學(xué)會(huì)走的時(shí)候,不要去跑。我自己也在迷茫中探索著自己的路,現(xiàn)在有點(diǎn)明白老子 所說的“企者不立,跨者不行”。

好了,廢話就此打住吧!只是想告訴你,其實(shí)編程并沒有那么困難,如果有什么東西沒明白,別著急,找基礎(chǔ)的東西去看。學(xué)好COM也一樣,看不懂的話,先把C++中的虛函數(shù)學(xué)明白,再去了解一下多線程的內(nèi)容。其實(shí)也沒那么復(fù)雜!

有人說,COM過時(shí)了,我也不清楚COM的將來會(huì)怎么樣,但我覺得理解一個(gè)東西總是有樂趣的。與你同勉。