[转载] 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的未来会怎么样,但我以为理解一个东西老是有乐趣的。与你同勉。

相关文章
相关标签/搜索