[轉載] COM 套間

http://www.vckbase.com/index.php/wv/1315php

簡序ios

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

企者不立,跨着不行。不少格言都告訴咱們作什麼事情都必須從基礎開始,對COM的理解也是這個道理。當三年前我看《COM 本質論》的時候,對虛函數也只是只知其一;不知其二,只是知道經過它能夠實現多態。但到底怎麼實現就不清楚了。看不懂COM太正常了。知道看過Stanley B.Lippman的《Inside the C++ Object Model》,對C++的內存結構有了基本的理解,我才明白了接口的意義。這篇文章是寫給初學者的,順便給你們一些建議,若是一本書你看不懂的時候,能夠 先放放,先找一些基礎的讀物來看看。這樣能夠少走一些彎路。編程

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

1、關於多線程(Multithreading)數組

子曰:本立道生。也就是說咱們明白事物所存在的緣由,天然也就明白事物是怎麼回事了。若是咱們清楚了套間(Apartment)的產生緣由,再去理解套 間,就容易許多了。咱們先來看看,爲何須要套間?套間是爲解決多線程中使用組件而產生的,首先咱們來了解一下多線程。 安全

一、理解進程(Processes)和線程(Threading)數據結構

理解線程,先從進程(Processes)開始,通常書上對進程的描述都比較抽象,都說進程是一個運行的程序的實例,進程擁有內存,資源。我這兒試着用一 段彙編程序來解釋一下進程,看看能不能幫你加深一下印象。咱們先來看一段簡單的彙編程序(你不理解彙編的話,建議找本書看看,一點不懂彙編,很難對其它高 級語言有太深的理解)。多線程

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

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

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

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

假如咱們的程序運行到mov eax,4,那麼EIP就會指向該句代碼所在的內存的地址。當這行代碼執行完畢以後,那麼EIP會自動加一,那麼它就會指向mov ebx,4。而程序的執行就是靠EIP的不斷增長來完成的(跳轉的話,EIP就變成了跳轉到的地址)。在Windows系統下,進程並不擁有 EIP,EAX,那麼只有進程,一個程序就沒法運行。而擁有這些寄存器的是線程,因此說進程是靜態的。

咱們知道一個CPU下只有一個EIP,一個EAX,也就是說同一時刻只能有一個線程能夠運行,那麼所說的多線程又是什麼呢?事實上同一時刻也只有一個線程 在運行,每一個線程運行一段時間後,它會把它擁有的EIP,EAX等寄存器讓出來,其它線程佔有這些寄存器後,繼續運行。由於這段時間很短,因此咱們感受不 出來。這樣咱們就能夠在一邊聽音樂的時候,一邊玩俄羅斯方塊了。爲了實現不一樣的線程之間的轉換,CPU要求操做系統維護一份固定格式的數據(該數據存在於 內存中),這份數據叫作Task-State Segment(TSS),在這份數據結構裏,維護着線程的EAX,EIP,DS等寄存器的內容。而CPU還有一個寄存器叫作Task Register(TR),該寄存器指向當前正在執行的線程的TSS。而線程切換事實上就是TR指向不一樣的TSS,這樣CPU就會自動保存當前的 EAX,EBX的信息到相應的TSS中,並將新的線程的信息加載到寄存器。

事實上線程不過上一些數據結構,這些結構保存了程序執行時候須要的一些信息。咱們能夠在windows提供的頭文件中找到一些影子,安裝VC後在它的 include目錄下有一個Winnt.h文件。在該文件中,咱們能夠找到這樣一個struct(_CONTEXT)。這就是線程切換時須要的數據結構 (我不肯定Windows內部是否用的就是這個結構,但應該和這份數據相差無幾)。

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

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

二、多線程存在的問題

咱們首先看一段多線程程序(該程序能夠在Code的MultiThreading中找到):

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

這段程序的本意是讓全局變量累次加10,並打印出操做後的數值。但咱們運行程序後的結果以下,能夠看到程序的運行結果非咱們所願。打印出的結果是一串亂序的文字。 

 

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

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

再次運行,結果就是咱們所須要的了。 

     

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

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

2、套間所要解決的問題   

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

咱們首先使用ATL建立一個簡單的組件程序,該程序有一個接口(ITestInterface1),該接口支持一個方法TestFunc1。(該組件能夠 在附加的源碼的「Apartment\TestComObject1」目錄下找到)咱們經過如下的程序調用該組件。(該程序能夠在附加的源碼的 「Apartment\ErrorUseApartment」目錄下找到)

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

該段程序將main中定義的ITestInterface1對象,經過指針傳到了新建的線程中。運行該段程序,結果以下,又是一串亂序的文字串。也就是說 咱們須要在TestComObject1中對TestFunc1進行線程同步控制。但大多數人並不想這樣作,由於咱們開發的組件大多數狀況下並不會在多線 程執行。但爲了不低機率事件發生後的不良後果,套間出場了。 

 

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

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

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

     

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

4、套間的類型

     

咱們首先看看ATL爲咱們提供的線程類型:Single,Apartment,Both,Free。咱們仍是經過例子來講明它們的不一樣。咱們仍然用咱們使用剛纔實現的TestComObject1來進行測試,先對它實現的惟一方法進行一下說明。

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

該方法很是簡單,就是打印出該方法運行時,所在的線程的ID號。若是在不一樣的線程中調用同一個對象的時候,經過套間,發送消息,最終該對象只應該在一個線程中運行,因此它的線程ID號應該是相同的。咱們將經過該ID值來驗證套間的存在。

一、Single

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

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

如下是運行結果: 

    

能夠看到,在main中咱們建立了一個ITestInterface1接口對象,並調用TestFunc1,此處會輸出一個線程 ID――ThreadID1。以後主線程生成一個線程,在該線程中,咱們會再次生成一個ITestInterface1接口對象,此處再次調用 TestFunc1,能夠看到輸出了另外一個線程ID――ThreadID2。由於是不一樣的對象,因此它們的線程ID號不一樣。(注意了,此處並無跨線程調 用對象,並不在套間的保護範圍)

好了,咱們該來看看Single類型的套間了。若是你和我同樣懶,不想爲此去寫一個single類型的接口,那麼打開你的註冊表。

    

找到咱們的接口ID,在InprocServer32項下,將ThreadingModel的值改成Single,或者將該項刪除(這樣也表明是Single套間)。咱們再來運行該程序,再看運行結果。 

 

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

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

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

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

咱們再來看一下運行結果。 

    

咱們能夠看到兩處調用TestFunc1,獲得的線程ID是相同的。咱們再經過VC的調試功能來看看第二個TestFunc1的運行過程。咱們在兩個 TesfFunc1調用處設置斷點,而後經過F11跟蹤進TestFunc1來看看它的調用過程。如下是在Main中的調用過程。

  

經過Call Stack,咱們能夠看到,此處是在main中直接調用的。咱們再來看第二處調用:

  

咱們能夠看到TestFunc1的調用須要經過一連串的API方法來實現。你感興趣的話,能夠經過反彙編的方法來跟蹤一下這些API,看看它們具體實現了 什麼,這裏咱們能夠看到這些函數在dll中的大體位置,你可使用W32DASM等反彙編工具打開這些dll,大體研究一下這些函數。

好了,咱們已經看到了Single套間的做用。那麼Single套間到底是什麼意思呢?就是說每一個被標誌爲Single的接口,在一個進程中只會存活在一 個套間中。該套間就是進程建立的第一個套間。你能夠將Main中與pTest相關的代碼都去掉,只保留CoInitializeEx(NULL, COINIT_APARTMENTTHREADED)和線程的建立,再次運行該程序,能夠發現建立線程中的TestFunc1仍然是經過消息來實現的。

好了看過了Single,咱們仍是在註冊表中,將ThreadingModel改成Apartment。經過修改註冊表就能夠實現對套間類型的控制,證實 了套間和咱們的程序自己沒有什麼關係,ATL的選項所作的做用也只是經過它來添加註冊表。套間只是對系統的一種提示,由COM API經過註冊表信息來幫咱們實現套間。

二、Apartment

在第二部分(套間所要解決的問題),咱們曾經提供了一個不一樣線程共享接口對象的方法,該方法是錯誤的(咱們也能夠經過程序阻止這種用法,稍候再敘)。此處咱們提供一種正確的作法。如下代碼在Apartment/Apartmenttest下能夠找到。

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

咱們經過CoGetInterfaceAndReleaseStream將main中的pTest變爲pStream,而後將pStream做爲參數傳入 到線程中,而後再經過CoGetInterfaceAndReleaseStream將pSteam變爲接口指針。再來看看運行的結果:

  

能夠看到兩次運行,線程ID是相同的。好的,咱們接着改變註冊表,再將Apartment變爲Free。而後再將兩處的HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);改成HRESULT hr = CoInitializeEx(NULL, COINIT_MULTITHREADED)。編譯後再次執行該程序,再來看執行結果。

  

咱們能夠看到兩個線程的ID是不一樣的。你能夠經過VC的Debug來看這兩組程序的TesFunc1的調用狀況,在第二種狀況下,建立的線程中不會經過消息機制來調用該函數。 

經過對比,咱們能夠知道所說的套間,就是經過消息機制來控制不一樣線程中對對象的調用。這樣就不須要組件的實現者來實現數據的同步。

三、Free

上節的例子,已經爲咱們提示了咱們Free套間,其實系統對咱們的組件不作控制,這樣就須要組件的開發者對數據的同步作出控制。

四、Both

所謂Both,就是說該對象既能夠運行在Apartment中,也能夠運行在Free套間中。該類型的前提是它應該是Free類型的套間,也就是說組件本身實現了數據的同步。而後設置成Both類型。 

爲何須要Both類型的套間呢?想一想假如咱們在咱們的組件中調用另外一個組件,這樣咱們就須要在咱們的組件中爲所調用的組件來開闢一個套間。咱們的套間是 一個Apartment,而調用的組件是Free類型的,這樣這兩個對象就必須存在於不一樣的兩個套間中。而跨套間的調用,須要經過中間代理來實現,這樣必 然會損失性能。但若是咱們調用的套間類型是Both的話,它就能夠和咱們的組件同享一個套間,這樣就能夠提升效率。

5、缺省套間

繼續咱們的測試,首先在註冊表中將咱們的接口類型改回Apartment。而後新建一個工程DefaultApartment。C++文件中的實現代碼以下。

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

此部分代碼與咱們測試Apartment時的代碼基本相同,只是新增了輸出main和建立線程的ID的語句。好的,咱們來運行程序,能夠獲得以下的結果:

 

咱們能夠看到main的線程ID和兩個TestFunc1的線程ID相同。也就是說兩個TestFunc1都是在main的線程中運行的。 

將咱們的程序作些變更,將CoInitializeEx(NULL, COINIT_APARTMENTTHREADED)改成 CoInitializeEx(NULL, COINIT_MULTITHREADED)。而後接着運行程序。咱們再來看運行的結果。

 

咱們能夠看到兩個TestFunc1的線程ID和main的不一樣了,和咱們建立的線程也不一樣。這是爲何呢?CoInitializeEx是一個建立套間 的過程,咱們使用CoInitializeEx(NULL, COINIT_MULTITHREADED)後,沒有爲咱們的組件建立合適的套間。這時候系統(也就是COM API,這裏應該是經過CoCreateInstance來實現的)就會幫咱們將咱們的接口對象放入缺省套間,該套間並不運行在當前的線程中。咱們再次在 Debug下跟蹤運行過程,能夠發如今main中調用TestFunc1,也須要經過衆多的API函數幫助完成,也就是說此處也是經過消息機制來完成的, 這樣性能上確定會有影響。

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

在第二部分咱們給出了一個經過直接傳輸接口指針到另外線程的例子,事實上這種方法是錯誤的,但COM API並無幫助咱們阻止這樣的錯誤。這個任務能夠由咱們本身來完成。

由於套間是和線程相關的,Apartment類型的接口方法只應該運行在一個套間中(其實這就是一個協議,並非強制性的),那麼咱們能夠經過線程的相關性質來實現。

在線程中咱們能夠經過Thread Local Storage(TLS)來保存線程的相關信息,同一函數運行在不一樣的線程中,那麼它所擁有的TLS也不相同。

咱們來動手改造咱們的類實現,將CTestferface1進行改造。

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

咱們先聲明一個私有成員變量dwTlsIndex,它用來存放TLS的索引值(一個線程的TLS至關於一個數組,能夠存放不一樣的數據)。再將構造函數中填 入保存數據的代碼。此處只是簡單的分配了一個字節的地址,並將該地址經過TlsSetValue保存到TLS中去。

而後再改造咱們的TestFunc1函數。以下:

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

這邊也很簡單,就是簡單的經過TlsGetValue去嘗試獲得dwTlsIndex所標誌的內容是否存在。若是不存在,那麼就說明程序運行在了不一樣的套 間中。就會返回RPC_E_WRONG_THREAD,這是COM設計者定義的宏,表示線程的非法使用。(因爲個人懶惰,再也不寫新的COM了,只是簡單的 修改了TestComObject1,這部分新加的代碼被我註釋掉了,你若是想看這部分的效果,去掉註釋就能夠了)

咱們再運行ErrorUseApartment程序,發現TestFunc1已經沒法輸出線程號,而是直接返回RPC_E_WRONG_THREAD。再次運行ApartmentTest程序,發現這樣的處理對它並無影響。仍然正常運行。

6、什麼是套間?

咱們從外部表現上對套間進行了瞭解,而套間到底是什麼?潘愛民譯的《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的未來會怎麼樣,但我以爲理解一個東西老是有樂趣的。與你同勉。

相關文章
相關標籤/搜索