天天看點

COM——套間

了解 COM 套間

junguo 下載下傳源代碼

簡序

  大學畢業前的最後一學期,在一家公司實習,當時的工作需要用到一些作業系統提供的元件。那時候隻知道COM這個名詞,并不知道到底是怎麼回事,隻知道上網到處找别人的源碼解決自己的問題;那段日子到現在回憶起來都是灰色的,每天呆坐在電腦前,一個網站一個網站的查找自己需要的源碼。但并不清楚自己到底在做什麼;那時候對自己能不能成為一個程式員充滿了懷疑。在實習結束返校的火車上,一夜間,我把一本《COM本質論》翻看了120多頁。當我和當時的女友吹噓自己一夜可以看100多頁書的時候,她馬上問我:看懂多少?當時我啞口無言。她忍受不了我那段日子的失落和抱怨,從那時候起,我們結束了那段簡短的感情。到如今我還在一個人漂泊着,而上周她成為了别人的妻子。想不到用什麼方式去紀念我迄今為止經曆過的唯一一段感情,我和她的感情并不完全是因為COM結束的,但由于對COM的迷惑,使我走向了迷茫,失落;對自己失去了信心,在她面前變成了一個悲觀失望的人。寫這篇文章權當對這份感情的一份紀念吧。

  企者不立,跨着不行。很多格言都告訴我們做什麼事情都必須從基礎開始,對COM的了解也是這個道理。當三年前我看《COM 本質論》的時候,對虛函數也隻是一知半解,隻是知道通過它可以實作多态。但到底怎麼實作就不清楚了。看不懂COM太正常了。知道看過Stanley B.Lippman的《Inside the C++ Object Model》,對C++的記憶體結構有了基本的了解,我才明白了接口的意義。這篇文章是寫給初學者的,順便給大家一些建議,如果一本書你看不懂的時候,可以先放放,先找一些基礎的讀物來看看。這樣可以少走一些彎路。

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

一、關于多線程(Multithreading)

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

    1、了解程序(Processes)和線程(Threading)

  了解線程,先從程序(Processes)開始,一般書上對程序的描述都比較抽象,都說程序是一個運作的程式的執行個體,程序擁有記憶體,資源。我這兒試着用一段彙程式設計式來解釋一下程序,看看能不能幫你加深一下印象。我們先來看一段簡單的彙程式設計式(你不了解彙編的話,建議找本書看看,一點不懂彙編,很難對其它進階語言有太深的了解)。

; 彙程式設計式示例
	data_seg segment  ;定義資料段
		n_i  dw   ?
	data_seg ends
	
	stack_seg segment ;定義堆棧
  	dw 128 dup(0)
	  tos label word
	statck_seg ends
	
	code1 segment   ;定義代碼段
	main proc far 
		assume cs:ccode,ds;data,seg,ss:stack_seg
	start:
	move ax,stack_seg   ;将定義的堆棧段的位址儲存到ss
	mov ss,ax
	mov sp,offset tos	  ;将堆棧的最後位址儲存到sp,堆棧是從下到上通路的
	
	push ds  ;儲存舊的資料段
	sub ax,ax
	push ax
	
	mov ax,data_seg		;将定義的資料段儲存到ds
	mov ds,ax
	
	call fact				;調用子函數
	
	…….				;其它操作省略
	ret  	;傳回到系統
	main endp
	
	fact proc near       ;子函數定義
		
			……				;具體操作省略
	ret  ;傳回到調用處
	fact endp
	
	code1 ends
		end start
				示例1:彙程式設計式結構
      

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

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

……
	mov eax,4
	mov ebx,5
	……
      

  假如我們的程式運作到mov eax,4,那麼EIP就會指向該句代碼所在的記憶體的位址。當這行代碼執行完畢之後,那麼EIP會自動加一,那麼它就會指向mov ebx,4。而程式的執行就是靠EIP的不斷增加來完成的(跳轉的話,EIP就變成了跳轉到的位址)。在Windows系統下,程序并不擁有EIP,EAX,那麼隻有程序,一個程式就無法運作。而擁有這些寄存器的是線程,是以說程序是靜态的。

  我們知道一個CPU下隻有一個EIP,一個EAX,也就是說同一時刻隻能有一個線程可以運作,那麼所說的多線程又是什麼呢?事實上同一時刻也隻有一個線程在運作,每個線程運作一段時間後,它會把它擁有的EIP,EAX等寄存器讓出來,其它線程占有這些寄存器後,繼續運作。因為這段時間很短,是以我們感覺不出來。這樣我們就可以在一邊聽音樂的時候,一邊玩俄羅斯方塊了。為了實作不同的線程之間的轉換,CPU要求作業系統維護一份固定格式的資料(該資料存在于記憶體中),這份資料叫做Task-State Segment(TSS),在這份資料結構裡,維護着線程的EAX,EIP,DS等寄存器的内容。而CPU還有一個寄存器叫做Task Register(TR),該寄存器指向目前正在執行的線程的TSS。而線程切換事實上就是TR指向不同的TSS,這樣CPU就會自動儲存目前的EAX,EBX的資訊到相應的TSS中,并将新的線程的資訊加載到寄存器。

  事實上線程不過上一些資料結構,這些結構儲存了程式執行時候需要的一些資訊。我們可以在windows提供的頭檔案中找到一些影子,安裝VC後在它的include目錄下有一個Winnt.h檔案。在該檔案中,我們可以找到這樣一個struct(_CONTEXT)。這就是線程切換時需要的資料結構(我不确定Windows内部是否用的就是這個結構,但應該和這份資料相差無幾)。

//
	// Context Frame
	//
	//  This frame has a several purposes: 1) it is used as an argument to
	//  NtContinue, 2) is is used to constuct a call frame for APC delivery,
	//  and 3) it is used in the user level thread creation routines.
	//
	//  The layout of the record conforms to a standard call frame.
	//
	
	typedef struct _CONTEXT {
	
  //
  // The flags values within this flag control the contents of
  // a CONTEXT record.
  //
  // If the context record is used as an input parameter, then
  // for each portion of the context record controlled by a flag
  // whose value is set, it is assumed that that portion of the
  // context record contains valid context. If the context record
  // is being used to modify a threads context, then only that
  // portion of the threads context will be modified.
  //
  // If the context record is used as an IN OUT parameter to capture
  // the context of a thread, then only those portions of the thread''s
  // context corresponding to set flags will be returned.
  //
  // The context record is never used as an OUT only parameter.
  //
	
  DWORD ContextFlags;
	
  //
  // This section is specified/returned if CONTEXT_DEBUG_REGISTERS is
  // set in ContextFlags.  Note that CONTEXT_DEBUG_REGISTERS is NOT
  // included in CONTEXT_FULL.
  //
	
  DWORD   Dr0;
  DWORD   Dr1;
  DWORD   Dr2;
  DWORD   Dr3;
  DWORD   Dr6;
  DWORD   Dr7;
	
  //
  // This section is specified/returned if the
  // ContextFlags word contians the flag CONTEXT_FLOATING_POINT.
  //
	
  FLOATING_SAVE_AREA FloatSave;
	
  //
  // This section is specified/returned if the
  // ContextFlags word contians the flag CONTEXT_SEGMENTS.
  //
	
  DWORD   SegGs;
  DWORD   SegFs;
  DWORD   SegEs;
  DWORD   SegDs;
	
  //
  // This section is specified/returned if the
  // ContextFlags word contians the flag CONTEXT_INTEGER.
  //
	
  DWORD   Edi;
  DWORD   Esi;
  DWORD   Ebx;
  DWORD   Edx;
  DWORD   Ecx;
  DWORD   Eax;
	
  //
  // This section is specified/returned if the
  // ContextFlags word contians the flag CONTEXT_CONTROL.
  //
	
  DWORD   Ebp;
  DWORD   Eip;
  DWORD   SegCs;        // MUST BE SANITIZED
  DWORD   EFlags;       // MUST BE SANITIZED
  DWORD   Esp;
  DWORD   SegSs;
	
  //
  // This section is specified/returned if the ContextFlags word
  // contains the flag CONTEXT_EXTENDED_REGISTERS.
  // The format and contexts are processor specific
  //
	
  BYTE    ExtendedRegisters[MAXIMUM_SUPPORTED_EXTENSION];
	
	} CONTEXT;
      

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

  2、多線程存在的問題

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

#include <iostream>
	#include <windows.h>
	
	int g_i = 10;  //一個全局變量
	
	DWORD WINAPI ThreadProc(LPVOID lpv)
	{
		g_i += 10;
		std::cout <<"In the Thread " << ::GetCurrentThreadId() << ",the first g_i is "  <<  g_i  <<  "!"  << std::endl;
		Sleep(5000); //睡眠
		g_i += 10;
		std::cout <<"In the Thread " << ::GetCurrentThreadId() << ",the secend g_i is "  <<  g_i  << "!" << std::endl;
		return 0;
	}
	
	int main(int argc, char* argv[])
	{
		
		DWORD threadID[2];
		HANDLE hThreads[2];
	
		for(int i = 0; i <= 1; i++ )			//建立兩個線程
			hThreads[i] = ::CreateThread(NULL,
  		0,
  		ThreadProc,
  		NULL,
  		0,
  		&threadID[i]);

	
		WaitForMultipleObjects(2,hThreads,TRUE,INFINITE);   //等待線程結束
	
		for(i = 0; i <= 1; i++ )
			::CloseHandle(hThreads[i]);				//關閉線程句柄
		system("pause");
		return 0;
	}
	示例程式2-多線程程式
      

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

COM——套間

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

#include <iostream>
	#include <windows.h>
	
	int g_i = 10;  //一個全局變量
	
	CRITICAL_SECTION cs;  //一個臨界區變量
	
	DWORD WINAPI ThreadProc(LPVOID lpv)
	{
		EnterCriticalSection(&cs);  //進入臨界區
	
		g_i += 10;
		std::cout <<"In the Thread " << ::GetCurrentThreadId() << ",the first g_i is "  << g_i <<  "!"  << std::endl;
		::LeaveCriticalSection(&cs);
		Sleep(5000); //睡眠
		EnterCriticalSection(&cs);
		g_i += 10;
		std::cout <<"In the Thread " << ::GetCurrentThreadId() << ",the secend g_i is "  << g_i << "!" << std::endl;
		::LeaveCriticalSection(&cs);
		return 0;
	}
	
	int main(int argc, char* argv[])
	{
		
		DWORD threadID[2];
		HANDLE hThreads[2];
		InitializeCriticalSection(&cs);
		for(int i = 0; i <= 1; i++ )			//建立兩個線程
				hThreads[i] = ::CreateThread(NULL,
			    0,
			    ThreadProc,
			    NULL,
			    0,
			    &threadID[i]);
	
		WaitForMultipleObjects(2,hThreads,TRUE,INFINITE);   //等待線程結束
		for(i = 0; i <= 1; i++ )
			::CloseHandle(hThreads[i]);				//關閉線程句柄
		
		system("pause");
		return 0;
	}
      

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

COM——套間

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

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

二、套間所要解決的問題

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

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

#define _WIN32_WINNT 0x0400
	#include <windows.h>
	#include <iostream>
	
	#include "../TestComObject1/TestComObject1_i.c"
	#include "../TestComObject1/TestComObject1.h"
	
	DWORD WINAPI ThreadProc(LPVOID lpv)
	{
	
		HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
	
		if ( FAILED(hr) )
		{
			std::cout << "CoinitializeEx failed!" << std::endl;
			return 0;
		}
	
		ITestInterface1 *pTest = NULL;
	
		hr = ::CoCreateInstance(CLSID_TestInterface1,
  			0,
  			CLSCTX_INPROC,
  			IID_ITestInterface1,
  			(void**)&pTest);
	
		if ( FAILED(hr) )
		{
			std::cout << "CoCreateInstance failed!" << std::endl;
			return 0;
		}
	
		hr = pTest->TestFunc1();
	
		if ( FAILED(hr) )
		{
			std::cout << "TestFunc1 failed!" << std::endl;
			return 0;
		}
		
		pTest->Release();
		::CoUninitialize();
		return 0;
	}
	
	int main(int argc, char* argv[])
	{
		HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
	
		if ( FAILED(hr) )
		{
			std::cout << "CoinitializeEx failed!" << std::endl;
			return 0;
		}
	
		ITestInterface1 *pTest = NULL;
	
		hr = ::CoCreateInstance(CLSID_TestInterface1,
  			0,
  			CLSCTX_INPROC,
  			IID_ITestInterface1,
  			(void**)&pTest);
	
		if ( FAILED(hr) )
		{
			std::cout << "CoCreateInstance failed!" << std::endl;
			return 0;
		}
		
		DWORD threadID;
		HANDLE hThreads  =   ::CreateThread(NULL, //建立一個程序
  		0,
  		ThreadProc,
  		NULL,  //将pTest作為一個參數傳入新線程
  		0,
  		&threadID);
		hr = pTest->TestFunc1();
		
		if ( FAILED(hr) )
		{
			std::cout << "TestFunc1 failed!" << std::endl;
			return 0;
		}
	
		::WaitForSingleObject(hThreads,INFINITE);	//等待線程結束
		::CloseHandle(hThreads);				//關閉線程句柄
		pTest->Release();
		::CoUninitialize();
		system("pause");
		return 0;
	}
      

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

COM——套間

三、套間如何實作資料的同步

  我們已經知道套間的目的是用來實作資料的同步,那麼套間如何來實作呢?如果我們能保證COM對象中的函數隻能在該對象中的另一個函數執行完以後,才能開始執行(也就是說元件中的函數隻能一個一個的執行),那麼我們的問題就可以解決了。是的,你可以發現,這樣的話,就失去了多線程的優勢;但套間的目的是保證小機率下的線程安全,損耗一些性能,應該比出現邏輯錯誤強點。

    那麼又如何保證同一對象下的所有方法都必須按順序逐個執行呢?微軟的COM API設計者們借用了Windows的消息機制。我們先來看一下windows的消息機制圖。

COM——套間

    我們可以看到所有線程發出的消息都回首先放到消息隊列中,然後在通過消息循環分發到各自視窗去,而消息隊列中的消息隻能一個處理完後再處理另一個,借助消息機制,就可以實作COM的函數一個一個的執行,而不會同時運作。Windows的消息機制是通過視窗來實作的,那麼一個線程要接收消息,也應該有一個視窗。 COM API的設計者在它們的API函數中實作了一個隐藏的視窗。在我們調用CoInitializeEx(NULL, COINIT_APARTMENTTHREADED)的時候,會生成這個視窗。(如果你對softice等動态調試工具熟悉的話,可以通過跟蹤源碼來跟蹤CoInitializeEx函數,可以發現它會調用API函數CreateWindowEx)。該視窗是隐藏的,有了這個視窗,就可以支援消息機制,就有辦法來實作對象中函數的逐一執行。這樣當對象指針被傳到其它線程的時候,從外部調用該對象的方法的時候,就會先發一個消息到原線程,而不再直接通路對象了。套間的原理大緻就是這樣。我們再來看看COM中的套間類型。

四、套間的類型

COM——套間

    我們首先看看ATL為我們提供的線程類型:Single,Apartment,Both,Free。我們還是通過例子來說明它們的不同。我們仍然用我們使用剛才實作的TestComObject1來進行測試,先對它實作的唯一方法進行一下說明。

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

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

  1、Single

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

#define _WIN32_WINNT 0x0400
	#include <windows.h>
	#include <iostream>
	
	#include "../TestComObject1/TestComObject1_i.c"
	#include "../TestComObject1/TestComObject1.h"
	
	DWORD WINAPI ThreadProc(LPVOID lpv)
	{
	
		HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
	
		if ( FAILED(hr) )
		{
			std::cout << "CoinitializeEx failed!" << std::endl;
			return 0;
		}
	
		ITestInterface1 *pTest = NULL;
	
		hr = ::CoCreateInstance(CLSID_TestInterface1,
  			0,
  			CLSCTX_INPROC,
  			IID_ITestInterface1,
  			(void**)&pTest);
	
		if ( FAILED(hr) )
		{
			std::cout << "CoCreateInstance failed!" << std::endl;
			return 0;
		}
	
		hr = pTest->TestFunc1();
	
		if ( FAILED(hr) )
		{
			std::cout << "TestFunc1 failed!" << std::endl;
			return 0;
		}
		
		pTest->Release();
		::CoUninitialize();
		return 0;
	}
	
	int main(int argc, char* argv[])
	{
		HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
	
		if ( FAILED(hr) )
		{
			std::cout << "CoinitializeEx failed!" << std::endl;
			return 0;
		}
	
		ITestInterface1 *pTest = NULL;
	
		hr = ::CoCreateInstance(CLSID_TestInterface1,
  			0,
  			CLSCTX_INPROC,
  			IID_ITestInterface1,
  			(void**)&pTest);
	
		if ( FAILED(hr) )
		{
			std::cout << "CoCreateInstance failed!" << std::endl;
			return 0;
		}
		
		hr = pTest->TestFunc1();
		
		if ( FAILED(hr) )
		{
			std::cout << "TestFunc1 failed!" << std::endl;
			return 0;
		}
		
		DWORD threadID;
		HANDLE hThreads[1];
		hThreads[0]  =   ::CreateThread(NULL,	//建立一個程序
			0,
			ThreadProc,
			(LPVOID)pTest,  //将pTest作為一個參數傳入新線程
			0,
			&threadID);
		
		::WaitForSingleObject(hThreads,INFINITE);	//等待線程結束
		::CloseHandle(hThreads);				//關閉線程句柄
		pTest->Release();
		::CoUninitialize();
		system("pause");
		return 0;
	}
      

  以下是運作結果:

COM——套間

  可以看到,在main中我們建立了一個ITestInterface1接口對象,并調用TestFunc1,此處會輸出一個線程ID——ThreadID1。之後主線程生成一個線程,在該線程中,我們會再次生成一個ITestInterface1接口對象,此處再次調用TestFunc1,可以看到輸出了另一個線程ID——ThreadID2。因為是不同的對象,是以它們的線程ID号不同。(注意了,此處并沒有跨線程調用對象,并不在套間的保護範圍)

  好了,我們該來看看Single類型的套間了。如果你和我一樣懶,不想為此去寫一個single類型的接口,那麼打開你的系統資料庫。

COM——套間

  找到我們的接口ID,在InprocServer32項下,将ThreadingModel的值改為Single,或者将該項删除(這樣也代表是Single套間)。我們再來運作該程式,再看運作結果。

COM——套間

  當列印出一個線程ID的時候,程式就停止了。Why?剛開始,我也被搞的頭暈腦脹。到MSDN中查找WaitForSingleObject,原來WaitForSingleObject會破壞程式中的消息機制,這樣在建立的線程中,TestFunc1需要通過消息機制來運作,消息機制破壞,就無法運作了。哎!還的再改程式。在查查《Win32多線程程式設計》,原來在GUI中等待線程需要用MsgWaitForMultipleObjects。好的,我們需要重新寫一個函數,專門用來實作消息同步。

DWORD ApartMentMsgWaitForMultipleObject(HANDLE *hHandle,DWORD dwWaitCout, DWORD dwMilliseconds)
	{
		BOOL bQuit = FALSE;
		DWORD dwRet;
		
		while(!bQuit)
		{
			int rc;
			rc = ::MsgWaitForMultipleObjects
				  (
					dwWaitCout, // 需要等待的對象數量
					hHandle,	// 對象樹組
					FALSE,		//等待所有的對象
					(DWORD)dwMilliseconds,  // 等待的時間
					(DWORD)(QS_ALLINPUT | QS_ALLPOSTMESSAGE)  // 事件類型    
				  );
			//等待的事件激發
			if( rc ==  WAIT_OBJECT_0 )
			{			
				dwRet = rc;	
				bQuit = TRUE;
			}
			//其他windows消息
			else if( rc == WAIT_OBJECT_0 + dwWaitCout )			
			{
				MSG msg;
				while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
				{
					TranslateMessage (&msg);
					DispatchMessage(&msg);
				}			
			} 
		}
		return dwRet;
	}
      

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

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

  我們再來看一下運作結果。

COM——套間

  我們可以看到兩處調用TestFunc1,得到的線程ID是相同的。我們再通過VC的調試功能來看看第二個TestFunc1的運作過程。我們在兩個TesfFunc1調用處設定斷點,然後通過F11跟蹤進TestFunc1來看看它的調用過程。以下是在Main中的調用過程。

  

COM——套間

  通過Call Stack,我們可以看到,此處是在main中直接調用的。我們再來看第二處調用:

  

COM——套間

  我們可以看到TestFunc1的調用需要通過一連串的API方法來實作。你感興趣的話,可以通過反彙編的方法來跟蹤一下這些API,看看它們具體實作了什麼,這裡我們可以看到這些函數在dll中的大緻位置,你可以使用W32DASM等反彙編工具打開這些dll,大緻研究一下這些函數。

  好了,我們已經看到了Single套間的作用。那麼Single套間究竟是什麼意思呢?就是說每個被标志為Single的接口,在一個程序中隻會存活在一個套間中。該套間就是程序建立的第一個套間。你可以将Main中與pTest相關的代碼都去掉,隻保留CoInitializeEx(NULL, COINIT_APARTMENTTHREADED)和線程的建立,再次運作該程式,可以發現建立線程中的TestFunc1仍然是通過消息來實作的。

  好了看過了Single,我們還是在系統資料庫中,将ThreadingModel改為Apartment。通過修改系統資料庫就可以實作對套間類型的控制,證明了套間和我們的程式本身沒有什麼關系,ATL的選項所做的作用也隻是通過它來添加系統資料庫。套間隻是對系統的一種提示,由COM API通過系統資料庫資訊來幫我們實作套間。

  2、Apartment

  在第二部分(套間所要解決的問題),我們曾經提供了一個不同線程共享接口對象的方法,該方法是錯誤的(我們也可以通過程式阻止這種用法,稍候再叙)。此處我們提供一種正确的做法。以下代碼在Apartment/Apartmenttest下可以找到。

#define _WIN32_WINNT 0x0400
	#include <windows.h>
	#include <iostream>
	
	#include "../TestComObject1/TestComObject1_i.c"
	#include "../TestComObject1/TestComObject1.h"
	
	DWORD WINAPI ThreadProc(LPVOID lpv)
	{
		//HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
		HRESULT hr = CoInitializeEx(NULL, COINIT_MULTITHREADED);
	
		if ( FAILED(hr) )
		{
			std::cout << "CoinitializeEx failed!" << std::endl;
			return 0;
		}
	
		IStream *pStream = (IStream*)lpv;
		
		ITestInterface1 *pTest = NULL;
	
		 hr = ::CoGetInterfaceAndReleaseStream(pStream,
    		IID_ITestInterface1,
    		(void**)&pTest);
		if ( FAILED(hr) )
		{
			std::cout << "CoGetInterfaceAndReleaseStream failed!" << std::endl;
			return 0;
		}
	
	
		hr = pTest->TestFunc1();
	
		if ( FAILED(hr) )
		{
			std::cout << "TestFunc1 failed!" << std::endl;
			return 0;
		}
		
		pTest->Release();
		::CoUninitialize();
		return 0;
	}
	
	DWORD ApartMentMsgWaitForMultipleObject(HANDLE *hHandle,DWORD dwWaitCout, DWORD dwMilliseconds)
	{
		
		BOOL bQuit = FALSE;
		DWORD dwRet;
		
		while(!bQuit)
		{
			int rc;
			rc = ::MsgWaitForMultipleObjects
				(
				dwWaitCout,    // 需要等待的對象數量
				hHandle,			// 對象樹組
				FALSE,				//等待所有的對象
				(DWORD)dwMilliseconds,  // 等待的時間
				(DWORD)(QS_ALLINPUT | QS_ALLPOSTMESSAGE)  // 事件類型    
				);
			
			if( rc ==  WAIT_OBJECT_0 )
			{			
				dwRet = rc;	
				bQuit = TRUE;
				
			}
			else if( rc == WAIT_OBJECT_0 + dwWaitCout )			
			{
				MSG msg;
				while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
				{
				  TranslateMessage (&msg);
				  DispatchMessage(&msg);
				}			
			} 
		}
		return dwRet;
	}
	
	int main(int argc, char* argv[])
	{
		//HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
		HRESULT hr = CoInitializeEx(NULL, COINIT_MULTITHREADED);
	
		if ( FAILED(hr) )
		{
			std::cout << "CoinitializeEx failed!" << std::endl;
			return 0;
		}
	
		ITestInterface1 *pTest = NULL;
	
		hr = ::CoCreateInstance(CLSID_TestInterface1,
  			0,
  			CLSCTX_INPROC,
  			IID_ITestInterface1,
  			(void**)&pTest);
	
		if ( FAILED(hr) )
		{
			std::cout << "CoCreateInstance failed!" << std::endl;
			return 0;
		}
	
		hr = pTest->TestFunc1();
	
		if ( FAILED(hr) )
		{
			std::cout << "TestFunc1 failed!" << std::endl;
			return 0;
		}
	
		IStream *pStream = NULL;
	
		hr = ::CoMarshalInterThreadInterfaceInStream(IID_ITestInterface1,
    			pTest,
    			&pStream);
	
		if ( FAILED(hr) )
		{
			std::cout << "CoMarshalInterThreadInterfaceInStream failed!" << std::endl;
			return 0;
		}
	
	
		DWORD threadID;
		HANDLE hThreads[1];
		hThreads[0]  =   ::CreateThread(NULL,			//建立一個程序
				    0,
				    ThreadProc,
				    (LPVOID)pStream,  //将pStream作為一個參數傳入新線程
				    0,
				    &threadID);
		ApartMentMsgWaitForMultipleObject(hThreads,1,INFINITE);
		::CloseHandle(hThreads);				//關閉線程句柄
		pTest->Release();
		::CoUninitialize();
		system("pause");
		return 0;
	}
      

  我們通過CoGetInterfaceAndReleaseStream将main中的pTest變為pStream,然後将pStream作為參數傳入到線程中,然後再通過CoGetInterfaceAndReleaseStream将pSteam變為接口指針。再來看看運作的結果:

  

COM——套間

  可以看到兩次運作,線程ID是相同的。好的,我們接着改變系統資料庫,再将Apartment變為Free。然後再将兩處的HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);改為HRESULT hr = CoInitializeEx(NULL, COINIT_MULTITHREADED)。編譯後再次執行該程式,再來看執行結果。

  

COM——套間

  我們可以看到兩個線程的ID是不同的。你可以通過VC的Debug來看這兩組程式的TesFunc1的調用情況,在第二種情況下,建立的線程中不會通過消息機制來調用該函數。

    通過對比,我們可以知道所說的套間,就是通過消息機制來控制不同線程中對對象的調用。這樣就不需要元件的實作者來實作資料的同步。

  3、Free

  上節的例子,已經為我們提示了我們Free套間,其實系統對我們的元件不做控制,這樣就需要元件的開發者對資料的同步做出控制。

  4、Both

  所謂Both,就是說該對象既可以運作在Apartment中,也可以運作在Free套間中。該類型的前提是它應該是Free類型的套間,也就是說元件自己實作了資料的同步。然後設定成Both類型。

    為什麼需要Both類型的套間呢?想想假如我們在我們的元件中調用另一個元件,這樣我們就需要在我們的元件中為所調用的元件來開辟一個套間。我們的套間是一個Apartment,而調用的元件是Free類型的,這樣這兩個對象就必須存在于不同的兩個套間中。而跨套間的調用,需要通過中間代理來實作,這樣必然會損失性能。但如果我們調用的套間類型是Both的話,它就可以和我們的元件同享一個套間,這樣就可以提高效率。

五、預設套間

  繼續我們的測試,首先在系統資料庫中将我們的接口類型改回Apartment。然後建立一個工程DefaultApartment。C++檔案中的實作代碼如下。

#define _WIN32_WINNT 0x0400
	#include <windows.h>
	#include <iostream>
	
	#include "../TestComObject1/TestComObject1_i.c"
	#include "../TestComObject1/TestComObject1.h"
	
	DWORD WINAPI ThreadProc(LPVOID lpv)
	{
		HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
		//HRESULT hr = CoInitializeEx(NULL, COINIT_MULTITHREADED);
	
		if ( FAILED(hr) )
		{
			std::cout << "CoinitializeEx failed!" << std::endl;
			return 0;
		}
	
		IStream *pStream = (IStream*)lpv;
		ITestInterface1 *pTest = NULL;
		 hr = ::CoGetInterfaceAndReleaseStream(pStream,
    		IID_ITestInterface1,
    		(void**)&pTest);
		if ( FAILED(hr) )
		{
			std::cout << "CoGetInterfaceAndReleaseStream failed!" << std::endl; 
			return 0;
		}
	
		std::cout << "ThradProc''s threadid is " << ::GetCurrentThreadId() << std::endl; //輸出ThradProc的線程ID
	
	
		hr = pTest->TestFunc1();
	
		if ( FAILED(hr) )
		{
			std::cout << "TestFunc1 failed!" << std::endl;
			return 0;
		}
		
		pTest->Release();
		::CoUninitialize();
		return 0;
	}

	DWORD ApartMentMsgWaitForMultipleObject(HANDLE *hHandle,DWORD dwWaitCout, DWORD dwMilliseconds)
	{
		
		BOOL bQuit = FALSE;
		DWORD dwRet;
		
		while(!bQuit)
		{
			int rc;
			rc = ::MsgWaitForMultipleObjects
				(
				dwWaitCout,    // 需要等待的對象數量
				hHandle,			// 對象樹組
				FALSE,				//等待所有的對象
				(DWORD)dwMilliseconds,  // 等待的時間
				(DWORD)(QS_ALLINPUT | QS_ALLPOSTMESSAGE)  // 事件類型    
				);
			
			if( rc ==  WAIT_OBJECT_0 )
			{			
				dwRet = rc;	
				bQuit = TRUE;
				
			}
			else if( rc == WAIT_OBJECT_0 + dwWaitCout )			
			{
				MSG msg;
				
				while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
				{
				  TranslateMessage (&msg);
				  DispatchMessage(&msg);
				}			
			} 
		}
		
		return dwRet;
	}
	
	int main(int argc, char* argv[])
	{
		HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
		//HRESULT hr = CoInitializeEx(NULL, COINIT_MULTITHREADED);
	
		if ( FAILED(hr) )
		{
			std::cout << "CoinitializeEx failed!" << std::endl;
			return 0;
		}
	
		ITestInterface1 *pTest = NULL;
	
		hr = ::CoCreateInstance(CLSID_TestInterface1,
  			0,
  			CLSCTX_INPROC,
  			IID_ITestInterface1,
  			(void**)&pTest);
	
		if ( FAILED(hr) )
		{
			std::cout << "CoCreateInstance failed!" << std::endl;
			return 0;
		}
	
		std::cout << "main''s threadid is " << ::GetCurrentThreadId() << std::endl;  //列印main的線程ID
	
		hr = pTest->TestFunc1();
	
		if ( FAILED(hr) )
		{
			std::cout << "TestFunc1 failed!" << std::endl;
			return 0;
		}
	
		IStream *pStream = NULL;
	
		hr = ::CoMarshalInterThreadInterfaceInStream(IID_ITestInterface1,
    			pTest,
    			&pStream);
	
		if ( FAILED(hr) )
		{
			std::cout << "CoMarshalInterThreadInterfaceInStream failed!" << std::endl;
			return 0;
		}
	
	
		DWORD threadID;
		HANDLE hThreads[1];
		hThreads[0] =   ::CreateThread(NULL,			//建立一個程序
				    0,
				    ThreadProc,
				    (LPVOID)pStream,  //将pStream作為一個參數傳入新線程
				    0,
				    &threadID);
	
		ApartMentMsgWaitForMultipleObject(hThreads,1,INFINITE);
		::CloseHandle(hThreads);				//關閉線程句柄
		pTest->Release();
		::CoUninitialize();
		system("pause");
		return 0;
	}
      

  此部分代碼與我們測試Apartment時的代碼基本相同,隻是新增了輸出main和建立線程的ID的語句。好的,我們來運作程式,可以得到如下的結果:

  

COM——套間

  我們可以看到main的線程ID和兩個TestFunc1的線程ID相同。也就是說兩個TestFunc1都是在main的線程中運作的。

    将我們的程式做些變動,将CoInitializeEx(NULL, COINIT_APARTMENTTHREADED)改為 CoInitializeEx(NULL, COINIT_MULTITHREADED)。然後接着運作程式。我們再來看運作的結果。

  

COM——套間

  我們可以看到兩個TestFunc1的線程ID和main的不同了,和我們建立的線程也不同。這是為什麼呢?CoInitializeEx是一個建立套間的過程,我們使用CoInitializeEx(NULL, COINIT_MULTITHREADED)後,沒有為我們的元件建立合适的套間。這時候系統(也就是COM API,這裡應該是通過CoCreateInstance來實作的)就會幫我們将我們的接口對象放入預設套間,該套間并不運作在目前的線程中。我們再次在Debug下跟蹤運作過程,可以發現在main中調用TestFunc1,也需要通過衆多的API函數幫助完成,也就是說此處也是通過消息機制來完成的,這樣性能上肯定會有影響。

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

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

  因為套間是和線程相關的,Apartment類型的接口方法隻應該運作在一個套間中(其實這就是一個協定,并不是強制性的),那麼我們可以通過線程的相關性質來實作。

  線上程中我們可以通過Thread Local Storage(TLS)來儲存線程的相關資訊,同一函數運作在不同的線程中,那麼它所擁有的TLS也不相同。

  我們來動手改造我們的類實作,将CTestferface1進行改造。

class ATL_NO_VTABLE CTestInterface1 : 
		public CComObjectRootEx
 ,
		public CComCoClass,
		public IDispatchImpl
	{
	private:
		DWORD dwTlsIndex; 
	public:
		CTestInterface1()
		{
			dwTlsIndex = TlsAlloc();
			HLOCAL l =  LocalAlloc(LMEM_FIXED, 1); 
			TlsSetValue(dwTlsIndex, l);    
		}
      

  我們先聲明一個私有成員變量dwTlsIndex,它用來存放TLS的索引值(一個線程的TLS相當于一個數組,可以存放不同的資料)。再将構造函數中填入儲存資料的代碼。此處隻是簡單的配置設定了一個位元組的位址,并将該位址通過TlsSetValue儲存到TLS中去。

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

STDMETHODIMP CTestInterface1::TestFunc1()
	{
		// TODO: Add your implementation code here
		LPVOID lpvData = TlsGetValue(dwTlsIndex);
		if ( lpvData == NULL )
			return RPC_E_WRONG_THREAD;
	
		std::cout << "In the itestinferface1''s object, the thread''s id is " << ::GetCurrentThreadId() << std::endl;
		return S_OK;
	}
      

  這邊也很簡單,就是簡單的通過TlsGetValue去嘗試得到dwTlsIndex所标志的内容是否存在。如果不存在,那麼就說明程式運作在了不同的套間中。就會傳回RPC_E_WRONG_THREAD,這是COM設計者定義的宏,表示線程的非法使用。(由于我的懶惰,不再寫新的COM了,隻是簡單的修改了TestComObject1,這部分新加的代碼被我注釋掉了,你如果想看這部分的效果,去掉注釋就可以了)

  我們再運作ErrorUseApartment程式,發現TestFunc1已經無法輸出線程号,而是直接傳回RPC_E_WRONG_THREAD。再次運作ApartmentTest程式,發現這樣的處理對它并沒有影響。仍然正常運作。

六、什麼是套間?

  我們從外部表現上對套間進行了了解,而套間究竟是什麼?潘愛民譯的《Com 本質論》說:套間既不是程序,也不是線程,然而套間擁有程序和線程的某些特性。我覺得,這句話翻譯的不到位,總讓人感覺套間似乎是和程序或者線程等同的東西。找來原文看看:An apartment is neither a process nor a thread; however, apartments share some of the properties of both。這裡的share被譯成了擁有,但我感覺此處翻譯為使用或者分享可能更貼切一些。不過原文事實上也很容易給初學者帶來誤導。其實套間隻是儲存線上程中的一個資料結構(還有一個隐藏着的視窗),借用該結構使套間和線程之間建立起某種關系,通過該關系,使得COM API通過該資訊可以建立不同套間中的調用機制。這部分涉及到列集,散集(我們調用CoMarshalInterThreadInterfaceInStream,CoGetInterfaceAndReleaseStream的過程)。在列集和散集過程中,COM API會幫我們建立一個不同套間中對象通信機制,這部分涉及到了代理,存根和通道的内容。通過代理來發送調用資訊,通過通道發送到存根,再通過存根調用實際的方法(其實那個隐藏的視窗就是為存根來服務的)。所做的這一切不過是為了實作不同套間中可以通過消息來調用對象。你可以找《Com 本質論》來看看,這部分的内容比較繁雜,但我感覺比起套間的概念,還是比較容易的。

  具體實作套間,線上程的TLS究竟儲存了什麼資訊呢?罪惡的微軟隐藏了這邊部分内容,我們無法得到這部分的材料。這可能也是套間了解起來如此困難的一個原因,套間呈現給我們的是一個抽象的概念。但了解其實際意義後,抽不抽象已經沒什麼關系,因為它所隐藏的不過是建立和使用套間時候繁雜的調用其它API函數的過程,事實上并沒有太多的神秘可言。對我們開發者來說,能明白套間的意義,已經足夠了。

  好了,稍微總結一下:套間是儲存線上程的TLS中的一個資料結構,通過該結構可以幫助不同的套間之間通過消息機制來實作函數的調用,以保證多線程環境下,資料的同步。

結語

  石康說:比爾.蓋茨并不是什麼天才,軟體工作者充其量不過是一個技術工作者,無法和科學工作者同日而語。石康還說:如果給他老人家足夠的時間,他也可以寫出一個作業系統。呵呵,大意好象如此,似乎是他老人家在《支離破碎》中的名言,現在記不太清楚了。剛開始覺得他老人家太狂了,不過仔細體會一下,确實如此。計算機的世界很少有真正高深的東西,有些内容你不了解,肯定是你的某方面的基礎不紮實。不了解接口,那是因為你的C++沒學好;不了解套間,那是因為你不懂多線程;不懂多線程那是因為你不懂CPU的結構。

  技術革新在眼花缭亂的進行的,.Net,Web services,到處閃現着新鮮的名詞,似乎這個世界每天都在變化的。但事實上,從286到386,從dos到圖形作業系統後,計算機再沒有什麼重大的革新。從我們開發者的角度來看,不過是開發工具的更新。但每次開發工具的更新都能使很多人興奮異常,激動着下載下傳安裝最新版本的工具,追逐着學習最新的開發語言。總覺的這樣就不會被時代所抛棄,總以為開發工具會幫着提升自己的價值。事實上呢?學會拖拉建立視窗的人,可能根本不知道Windows中有一個消息機制。開發十多年的人會把一個棧中生成的對象的位址作為參數傳給接收者。沒有學會走的時候,不要去跑。我自己也在迷茫中探索着自己的路,現在有點明白老子所說的“企者不立,跨者不行”。

  好了,廢話就此打住吧!隻是想告訴你,其實程式設計并沒有那麼困難,如果有什麼東西沒明白,别着急,找基礎的東西去看。學好COM也一樣,看不懂的話,先把C++中的虛函數學明白,再去了解一下多線程的内容。其實也沒那麼複雜!

有人說,COM過時了,我也不清楚COM的将來會怎麼樣,但我覺得了解一個東西總是有樂趣的。與你同勉。  

繼續閱讀