簡(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è)東西總是有樂趣的。與你同勉。