描述:
有一个COM编写的DLL,不知道是STA还是MTA,主要功能是串口通信(PC与手机之间),不知道这一COM是
STA(single-threaded apartment)还是MTA(multi-threaded apartment),初始化用CoInitialize(NULL)没什么
问题,用CoInitialzeEx(NULL,COINIT_MULTITHREADED)编译出错,错误就是error C2065: 'CoInitializeEx',
‘COINIT_MULTITHREADED’ : undeclared identifier。若使用
CoInitialzeEx(NULL,COINIT_APARTMENTTHREADED)则就是CoInitializeEx是undeclared identifier。
我现在是想要用这一个DLL实现一拖八,就是说一台PC机上若有8个串口,这8个串口就要同时使用这一DLL实现通讯功
能。我直接用AfxBeginThread(ThreadFunc,(LPVOID)pPort)开线程,若是初始化COM是在ThreadFunc里实现的话,
则能够顺利调用里面的函数,但是收不到手机回应到PC机的消息,所以不知道送出去的对不对。若是初始化放在线程
外,则调用DLL函数时的HRESULT值是0x8001010e,查询MSDN给的解释是:
You will receive the following error message when an apartment threaded component uses a single
threaded object。
这一拖八应该如何实现? 开线程的方式有误吗?
解决方案1:
不同线程间调用同一个接口指针会有下面错误:
应用程序调用一个已为另一线程整理的接口。
如前面所说,COM的套间机制要成功,必须服务器(组件)、客户和COM运行时期库三方面合力实现,其中有任何一方不按着规矩来,将不能实现套间机制的功能,不过这并不代表什么错误,套间机制不能运作并不代表程序会崩溃,只是不能和其他COM应用兼容而已。
比如:对象中的属性1在你设计的算法中肯定不会被两个以上的线程写入,只是会被多个线程同时读出而已,因此不用同步,可以用MTA,但对象的属性2却可能被多个线程写入,因此你决定使用STA。因此在客户端,通过前面说的CoMarshalInter...()和CoGetInterf...()将对象指针传到那个只会写入对象的属性1的线程,其实这时就可以直接将对象指针传到这个线程,而不用想上面那样麻烦(而且增加了效率),但是就破坏了COM的套间规矩了——两个线程可以访问对象,但对象在STA套间中。所以?!!什么事都不会发生,因为你已经准确知道你的算法不会捅娄子(线程访问冲突),即使破坏COM的规矩又怎样?!而且组件仍可以和其他客户兼容,因为不按规矩来的是客户,与组件无关。不过如果组件破坏规矩,那么它将不能和每一个客户兼容,但并不代表它和任何客户都不兼容。这里其实就是客户和组件联合起来欺骗了COM运行时期库。
我只是想说明规则是拿来用的,不是拿来栓自己的。客户要做的工作前面已经说过了(那两个函数或全局接口表或其他只要正确的方式),下面说明组件应该做的工作。组件可以存在在四个套间中(多了一个主线程套间),所需工作分别如下:
STA——当一个组件是STA时,它必须线程保护全局变量和静态变量,即对全局变量和静态变量的访问应该用临界段或其他同步手段保护,因为操作全局和静态变量的代码可以被多个STA线程同时执行,所以那些代码的地方要进行保护。比如对象计数(注意,不是引用计数),代表当前组件生成的对象个数,当减为零时,组件被卸载。此变量一般被类厂对象使用,还好ATL和MFC已经帮我们实现了缺省类厂,这里一般不用担心,但自定义的全局或静态变量得自己处理。
主STA——与STA唯一的不同是这是傻瓜型的,连静态和全局变量都可以不用线程保护,因为所有不是安全访问静态和全局变量的对象都通过主线程的消息派送机制运行,因此不安全的访问都被集中到了一个线程的调用中,因而调用被序列化了,也就实现了对静态和全局变量的线程保护。至于为什么是主线程,因为进程被创建的时候一定会创建主线程,所以一定可以创建主STA。因此主STA并不是什么第四种套间,只是一个STA套间,不过关联的是主线程而已,由于它可以被用做保护静态和全局变量而被单独提出来说明。因此一个进程内也只有一个主STA套间。
MTA——必须对组件中的每个成员和全局及静态变量的访问使用同步手段进行保护,还应考虑线程问题,即不是简单地保护访问即可,还应注意线程导致的错误的操作,最经典的就是IUnknown::Release().
DWORD IUnknown::Release()
{
DWORD temp = InterlockedDecreament( &m_RefCount );
if( !temp ) // 不能用m_RefCount,原因楼主还是自想吧
delete this; // 因此不是只要用原子访问函数保护了m_RefCount的访问就行了
return temp; // 前面对全局变量的保护也和此类似,要考虑线程问题
} 如果楼主对自己多线程编程的技术没有信心,建议最好不要编写可以存在于MTA套间的组件,不过就不能获得MTA的高性能了。
在编写MTA时还应该注意到线程相关性。没有线程相关性是指没有任何线程范围的成员变量,比如线程局部存储(TLS,Windows提供的一种机制,即分配的一块内存,这块内存只能被执行的线程所访问到,即使指向这块内存的指针是p,但不同的线程调用i = *p;将会将不同的值存到i中。不过这块内存一般很小,几十个四字节,所以一般new一块内存,然后将地址放到那四个字节中,就相当于每个线程都有一块属于它们的私有内存空间)。
也就是说在MTA中不能保存任何记录着TLS内存的指针,将没有意义(比如A线程记录的内存空间对B线程来说是无效的,因为TLS构造了一个线程相关的内存空间,就象每个进程都有自己的私有空间)。而不幸地MFC在它的底层运作机制的实现中大量使用了TLS,如模块线程状态、线程状态等。正是由于这个原因,MFC不能编写在MTA中运行的组件。
NA——前面关于NA的实现那里说错了:“是STA套间,将调用转成消息发送给调用线程;是MTA套间则什么事都不发生”,应该是当NA对象里有个对指向一个STA对象的指针的调用时,将会将调用转成向被调用的STA对象的关联线程发送消息,如果此时调用NA对象的是另一个STA线程或MTA线程,照样会发生线程切换。同理,如果那个对象是MTA的,而不是STA的,依旧发生线程切换(只有当调用线程是STA线程时)。不过处此以外的大多数情况(即不在NA对象的方法中调用另一个套间对象的方法)都不会发生线程切换,即使出现上面的情况也只有必要(MTA调NA再调MTA就不用切换)才切换线程。
在编写运行于NA套间的组件时,只需满足MTA的要求即可,上面的NA实现将由它的轻量级代理和COM运行时期库共同完成,程序员不用操心。
前面提到过有一种进程内组件可以被指明为Both的ThreadModel,这种组件很象NA,哪个套间都可能直接访问它,但只是可能,而NA组件是可以,这点可以从前面的那个进程内组件所属套间的规则中看出。这种组件可以支持一种称作自由线程汇集器的技术,这点很简单,楼主还是自己看书吧。当Both的组件使用了自由线程汇集器时,除了满足MTA的要求以外(上面所说的线程安全保护和没有线程相关性),还要记录传进来的接口指针的中立形式(比如IStream*,可以通过CoMarshallInter...()或GIT得到),以防止对客户的回调问题,具体原因楼主还请参考相关资料。
最后只是提醒一下,有3个STA套间,STA1、STA2和STA3,STA1用CoMarshallInter...()得到的IStream*传到STA2中被GetInterface...()得到的代理和在STA3中得到的代理不同,不能混用。因为当STA2和STA3调用在STA1的对象时,STA1如果回调(连接点技术就是一种回调)调用者,则STA2和STA3的代理能分别正确的指出需要让哪个线程执行回调操作,即向哪个线程发送消息,因此不能混用。
线程在进行大多数COM操作之前,需要先调用CoInitialize或CoInitializeEx。调用CoInititalize告诉COM生成一个STA套间,并将当前的调用线程和这个套间相关联。而调用CoInititalizeEx( NULL, COINIT_MULTITHREADED );告诉COM检查是否已经有了一个MTA套间,没有则生成一个MTA套间,然后将那个套间和调用线程相关联。接着在调用CoCreateInstance或CoGetClassObject等创建对象的函数时,创建的对象将以一个特定规则决定和哪个套间相关联(后叙)。这样完成后,就完成了线程、对象和套间的关联(或绑定)。
前面提到的决定对象去向的规则如下。
当是进程内组件时,根据注册表项ThreadModel(可能记错了)和线程的不同,列于下:
创建线程关联的套间种类 ThreadModel 组件最后所在套间
STA Apartment 创