志于道,据于德,依于仁,游于艺 ——《论语•述而》
大巧在所不为,大智在所不虑。所志于天者,已其见象之可以期者矣;所志于地者,已其见宜之可以息者矣 ——《荀子•天论》
keyword:
C++, virtual inheritance, template;
Java, JVM, method table, thread, security;
COM, event, IDispatch, apartment, SCM, MTS, database;
.net, OO, MCMS
先从C++说起。C++学的时间最长,不仅周老师讲过,户sir也讲,侯捷也讲。。对象结构啊STL啊都讲烂了,实在是没什么新东西来写。说说虚拟继承吧。虚基类被作为一个单独的部分。写段程序就可明白虚拟继承的语意。编译器是简单方便的VC++6.0
1 虚拟继承使得不能通过父类的指针来访问子类,Base1虚拟继承了Root,
Root * pR = new Base1;报错:
Compiler Error C2243 'conversion type' conversion from 'class Base1 *' to 'class Root *' exists, but is inaccessible
2 如果改为Base1 * pR = new Base1;不报错,但是访问子类没有改写的虚函数会报错:no accessible path to public member declared in virtual base 'Root'
3 定义两个类:class Base1 : virtual Root, class Base2 : virtual Root, class Derived : public Base1, public Base2,如果Base1,Base2 中均改写了Root中某个虚函数,编译报错:ambiguous inheritance。只要把其中任一个虚拟继承改成public,就不报错。
4 将Base1的定义改为:class Base1 : public Root2, virtual Root
其中Root2也带虚函数,如果是一般的多重继承,如果VC++6是采用的N表结构,也就是多重继承中有几个带虚函数的父类就有几张虚函数表,那么Root的虚函数表应该追加到第一个带虚函数的父类的虚函数表中来作为整个继承类的虚函数表。但结果还是不能调用未改写的虚父类中的虚函数。
以上4点充分说明了虚基类被作为一个单独的部分。为什么第一条中不能调整指针?因为偏移值无法确定。为什么第三条中不能同时改写同一个虚基类中的虚函数?因为对应这个共用的虚基类只有一份虚函数表,不像多重继承,每个带虚函数的都有自己的表。
对于调整指针,设想两个类Base1,Base2都虚拟继承了Root,而某个类多重继承Base1 ,Base2,则这两个类的开始处与虚基类的偏移值肯定是不同的,只有一份虚基类被构造,所以根本没法调整。不含虚继承的多重继承中调整指针的例子:
如class Derived : public Base1, public Base2
则Base2 *pbase2 = new Derived; 会编译为:
Derived *temp = new Derived;
Base2 *pbase2 = temp ? temp + sizeof( Base1 ) : 0;
要让指针指向Base2子对象。为此发展出的thunk技术使得调用虚函数尤其析构函数时先将指针调整到合适位置。
说到继承,据说Sun公司的Mike Ball主持设计的C++编译器在多重继承时是一个虚函数表,不是N表。我想这对最终Java的method table的想法产生影响,Java对象也是一张表。本文下一部分将详细讲Java对象。
哀悼模板
模板给了我们不同于继承的重用机制,表达的语意是基于特征兼容的多态。特征兼容就是说,如果传入的模板参数T在内部被这样使用T[],那这个类必须有random访问算符才行。Stanley Lippman称赞模板是面向对象的高峰。确实,有些模板的巧妙用法让我感慨万分。《ATL Internels》上仿真动态绑定的例子:
[quote]
template
class Array{
public:
…
bool operator<(const Array& rhs)
{ return static_cast(this)->Compare(rhs) < 0 }
...
T m_rg[1024];
};
[/quote]
以后的派生类就可以这么写了:
[quote]
class String : public Array{
public:
int Compare(const Array& rhs)
{return strcmp(m_rg, rhs.m_rg);}
};
[/quote]
这样不用分配虚函数指针就能仿真实现多态了。看完这例子俺感慨道:不是缺少美,而是缺少创造--可惜这么强悍的东西要消失了。Java就没有真正的模板,只是种形式。C#最近才支持模板。为什么模板会消失?单根体系的出现使得不用模板就可解决问题。任何对象都可看作Object类的引用,类里增加个成员Object m_o,就可代替typename T,于是我们对模板说再见。消失的不仅仅是模板,.net的System.Collections包里不再需要链表了。。反正放的都是引用,又不是大对象。还有熟悉的iterator,它存在的意义就在于让内存布局不同的各种容器看起来都像数组。该消失的总会消失。就在此哀悼下C++吧,一个时代在慢慢地,悄悄地,结束,直到若干年后,没人会记起那些曾经辉煌的名字。
从Omale处得知C#的新版本支持模板时我很惊讶,这个世界太疯狂了,暑假前C#还不支持模板呢。这回是真正的模板,不是像Java中的伪模板,而且能加上限制,比如实现了A接口的类才能用到模板参数中。“基于特征兼容的多态”啊,“实现某接口”正是特征。为什么又需要模板了?单根体系带来向下转型的问题,而模板是参数化类型,避免了转型。
[b]
[color=#ff0000]2. Java——我歌月徘徊,我舞影凌乱
[/color]
[/b]取几捧炉灰,摩西要在法老面前向天扬起来。这灰要在埃及全地变作尘土 ——《圣经•出埃及记》
乘舲船余上沅兮,齐吴榜以击汰。船容与而不进兮,淹回水而凝滞 ——《涉江》
Java是Sun无意中点燃的星星之火,烧掉了Microsoft的半壁江山。推动语言发展的有硬件的发展,网络的发展。Java正是应运而生。Java是美的,Java不仅仅是种语言,也是一种体系架构。编译器将源代码编成二进制码,然后在不同平台的虚拟机上运行。最初是为了跨平台,所以JVM的指令集尽量多用栈,使得用的寄存器组简单,便于移植,Java之父原来在家用电器的处理器上编过程序--Java提高了开发效率,不仅因为跨平台避免重复开发,还有自动内存管理,不用担心memory leak,Java不执行移动代码,以及会将所有值初始化,还有Java是强类型的等等,这些能很大程度避免编程中的错误,当然提高了开发效率。但是执行效率却很难和C++比。拿调用类的非static公有成员函数相比:如果C++中调用非虚函数,那是直接一个CALL指令跳过去,如果是虚函数,先到表中找指针,再跳过去,开销最多就是一次指针运算而已。而Java呢?这要从Java对象说起。正如C++对象的结构是由编译器厂商决定的,Java对象的结构由虚拟机的实现者来决定

一种常见的对象结构,引用指向的结构中包含指向method table的指针,堆中保存实例数据,就是非static的数据。方法表一个类的所有对象共用一个,保存在method area。method data中包含方法栈大小,二进制码,异常表(不管C++还是Java,我从没关心过异常是如何实现的)。上图有问题,这样的结构在垃圾收集时不方便。在堆中移动了某对象后所有指向该对象的引用都要重新定位。一般会有抽象一层的实现,对象引用指向一个“handle pool”,这里面保存的指针指向实例数据和方法数据等。这样堆中对象移动后只改掉handle pool中的值就行了。method table中包含了从父类继承及所实现接口的非static的公有函数。指针指向一定的结构可以让虚拟机知道到何处调用该方法。如果方法第一次调用,虚拟机会将二进制码编译成本地可执行代码。下次就可直接调用了。而动态链接的时候会进行所有父类及接口的解析,构造出method table,动态链接时method area中的方法改成一个方法表中的偏移值。可以看出调用的开销了。调用方法的第一步是取得handle的值,第二步根据方法表的入口和偏移值来获得这个方法的指针,第三步根据第二步获得的指针来dereference一个结构,虚拟机根据这个结构包含的信息来知道方法有没有编译过,以及二进制码保存在哪里,如编译了会保存可执行码的指针,根据这个指针调用方法。如果是通过接口来调用,效率会更低。由于Java中没有多重继承,而可以实现多个接口,编译时产生的指令不同,通过invokevirtual来掉用类的函数,invokeinterface来调用接口函数。如果通过接口调用,虚拟机会去遍历整个方法表来找合适的方法,而通过指向类的引用来调用只用固定偏移值就可以了,为什么接口不能固定偏移值?因为可以实现多个接口,不能确定这个方法在这个类的method table里偏移值。一个类只能有一个父类。父类的函数被放在子类method table的最前面。也就是说一个Java类的方法表的第一个指针肯定是clone(),下一个指针一定是equals(Object)。。因为所有的类都会继承Object类。
据说Sun公司的Mike Ball主持设计的C++编译器在多重继承时只有一张虚函数表,除第一个基类以外的基类的虚函数指针会保留个偏移值,让编译器插入调整指针的代码来找到虚函数表中自己开始的位置。Stanley Lippman描述的C++编译器多重继承是N张表,不管几张表,能表达multiple inheritance的语意就行。Java中的method table应该借鉴了Sun的C++实现。
线程是我感兴趣的话题。
每个Java application运行在一个虚拟机的实例中,有N个应用程序就有N个虚拟机实例。实例中有多个线程,其中有某个daemon thread用于垃圾收集。Java application中的main不是真正的main,JVM的线程是主线程,它等待其他线程的结束。SUN为WINDOWS的开发的虚拟机中,用户写的线程是内核级线程。写段程序,打开任务管理器,运行一个单线程Java程序,结果任务管理器显示OS中增加了将近10个线程。多建几个线程,thread类的run方法是打印N遍自己的编号。运行,显示OS中峰值时增加了大概20个线程
Thread t[] = new Thread[10];
for(i =0; i < 10; ++i)
{
t[i]= new thread(i);
t[i].start();
}
用户写的线程确实是内核级线程。在有的虚拟机实现中会是用户级线程。值得一提的是每个Java对象都和一个wait set联系起来。Java的根类Object中含有的wait,notify方法可以实现线程同步。而阻塞的线程就保留在该对象的wait set中。这让我想起windows内核对象,什么WaitForSingleObject。不过windows内核不会拣垃圾。根据不同的垃圾收集算法,每个Java对象都会保留信息供垃圾收集器使用。
再谈Java安全性
Java的安全性有很多体现:Java要求class loader在装载代码时要检查代码是不是正确。一个类要求装载的其他类会由同一个class loader object装载并放在同一个名字空间中。对网络小程序有诸多限制,默认情况下面这些操作都不能做:读写磁盘,创建进程,尝试与这个applet来源主机以外的主机建立连接等等。可以通过java.security定制安全政策,没研究过具体细节,貌似比较复杂。
Java不执行移动代码,这是多明智的做法啊。
char szBuf[100];
szBuf[10000] = 0;
上面的代码在C++会导致很难找出的错误,Java中则会抛出异常。WIN98中的线程堆栈在前后各有64K保护属性的页面用于捕捉堆栈溢出。WIN2K中仅在栈底部有保留属性的页面,如果线程试图访问该页面,系统就会终止整个进程的运行。甚至不蹦出个对话框,整个进程都结束了,写递归程序时经常这样--缓冲区溢出是黑客的常用手段,但很多时候是我们自己毁了自己的程序。
Java的栈很特别。一个线程有一个Java stack。“When a thread invokes a Java method, the virtual machine creates and pushes a new frame onto the threadís Java stack”——《inside JVM》。Java中“方法帧”的存在是不是意味着线程栈的大小可以变化?C++编译时有选项指定线程默认栈大小,C++中创建线程时也可以指定栈大小,而线程栈大小一旦指定不可变,且逻辑上是连续的。根据Bill Venners的说法,Java stack逻辑上都可不连续。书上有张图,Java stack中包含了N个方法帧,像链表一样连起来,嵌套调用方法时就连入个新帧。
Java带来的全新理念,just-in-time compiling, garbage collection等等,深深影响了后来的.net。如果说.net看的更远,那是因为它站在巨人的肩膀上。
[/i][/i]
[b]
[color=#ff0000]3. COM——蓦然回首,那人却在灯火阑珊处
[/color]
[/b]君子务本,本立而道生 ——《论语•学而》
东方明矣,朝既昌矣。匪东方则明,月出之光 ——《诗经•齐风•鸡鸣》
志不强者智不达 ——《墨子•修身》
Don Box《COM本质论》第一章的题目便是“COM是一种更好的C++”。他在那本书中已比较过了,COM的封装性如何好,可移植性如何好。批评COM的人说COM对继承性的支持不好。每次都要依靠包容和聚合来实现复用,确实麻烦,连如何增加减少引用计数都要考虑好。
不过我们应该感激而不是抱怨。微软的编译器及操作系统为COM提供了强大支持:
C++利用MFC或ATL点几下按钮就可以生成个组件;
MFC为组件自动生成包装类;
编译器可以直接生成代理和存根,使我们的组件轻松跨越套间边界;
拖个ActiveX到VB的Form上,编译器的属性页马上通过接口暴露出组件的属性;
根据CLSID本地或跨主机创建组件及获得组件(CoGetClassObject),管理对套间的并发访问,通过各种协议(主要是UDP)与远程组件通信,分布式垃圾收集等等等等,Windows操作系统早已封装了各种服务。
微软处处为程序员着想--有了强大的编译器和强大的操作系统,写组件和用组件不是难事
候捷老师在《深入浅出MFC》中剖析了MFC架构。MFC封装了COM。我特意研究了下MFC中如何封装以及支持ActiveX:
首先我用MFC编写了个ActiveX,取名叫FirstAx。详细做法参见户sir课件。
在另外一个MFC对话框工程中使用该控件,根据Get/Set属性生成了包装函数。观察其set函数,大体如:
void CFirstAx::SetFlag(short propVal)
{
SetProperty(0x3, VT_I2, propVal);
}
设置断点F10/11跟入,发现包装类CFirstAx中的SetProperty函数首先调用COleControlSite::InvokeHelperV
在该函数中查询组件的分发接口:m_pObject->QueryInterface(IID_IDispatch,(LPVOID*)&pDispatch)
然后调用COleDispatchDriver::InvokeHelperV,该函数中首先整理参数,然后发出实际调用:
// make the call
SCODE sc = m_lpDispatch->Invoke(dwDispID, IID_NULL, 0, wFlags,
&dispparams, pvarResult, &excepInfo, &nArgErr);
这就是MFC对ActiveX中的属性封装。万变不离其宗,转来转去最终掉用了Invoke
再谈MFC如何支持ActiveX事件:
首先看几个宏:
在AfxWin.h中找到的DECLARE_EVENTSINK_MAP在非DLL中的定义:
[quote]
#define DECLARE_EVENTSINK_MAP() \
private: \
static const AFX_EVENTSINKMAP_ENTRY _eventsinkEntries[]; \
static UINT _eventsinkEntryCount; \
protected: \
static AFX_DATA const AFX_EVENTSINKMAP eventsinkMap; \
virtual const AFX_EVENTSINKMAP* GetEventSinkMap() const; \
[/quote]
在AfxDisp.h中找到的相关内容:
[quote]
#define BEGIN_EVENTSINK_MAP(theClass, baseClass) \
const AFX_EVENTSINKMAP* theClass::GetEventSinkMap() const \
{ return &theClass::eventsinkMap; } \
const AFX_EVENTSINKMAP theClass::eventsinkMap = \
{ &baseClass::eventsinkMap, &theClass::_eventsinkEntries[0], \
&theClass::_eventsinkEntryCount }; \
UINT theClass::_eventsinkEntryCount = (UINT)-1; \
const AFX_EVENTSINKMAP_ENTRY theClass::_eventsinkEntries[] = \
{ \
#define END_EVENTSINK_MAP() \
{ VTS_NONE, DISPID_UNKNOWN, VTS_NONE, VT_VOID, \
(AFX_PMSG)NULL, (AFX_PMSG)NULL, (size_t)-1, afxDispCustom, \
(UINT)-1, 0 } }; \
#define ON_EVENT(theClass, id, dispid, pfnHandler, vtsParams) \
{ _T(""), dispid, vtsParams, VT_BOOL, \
(AFX_PMSG)(void (theClass::*)(void))&pfnHandler, (AFX_PMSG)0, 0, \
afxDispCustom, id, (UINT)-1 }, \
[/quote]
都学过MFC了,上面的宏很好理解。总之,编程时点那几个按钮来生成响应事件代码的时候,事件像消息一样被列成了表格。
事件是依靠连接点机制来回调函数的。
包含ActiveX的对话框在创建组件时调用了:
[quote]
HRESULT COleControlSite::CreateControl(CWnd* pWndCtrl, REFCLSID clsid,
LPCTSTR lpszWindowName, DWORD dwStyle, const POINT* ppt, const SIZE* psize,
UINT nID, CFile* pPersist, BOOL bStorage, BSTR bstrLicKey)
[/quote]
该函数中首先调用了:
[quote]
HRESULT COleControlSite::CreateOrLoad(REFCLSID clsid, CFile* pFile,
BOOL bStorage, BSTR bstrLicKey)
[/quote]
这个函数又调用:
[quote]
_AfxCoCreateInstanceLic(clsid, NULL,
CLSCTX_INPROC_SERVER | CLSCTX_INPROC_HANDLER, IID_IOleObject, (void**)&m_pObject, bstrLicKey)
[/quote]
从而创建了COM组件
创建组件完毕后,回到CreateControl函数中,该函数中有一句:
m_dwEventSink = ConnectSink(m_iidEvents, &m_xEventSink);
非常关键的函数。F11跟进去后:
[quote]
DWORD COleControlSite::ConnectSink(REFIID iid, LPUNKNOWN punkSink)
{
ASSERT(m_pObject != NULL);
LPCONNECTIONPOINTCONTAINER pConnPtCont;
if ((m_pObject != NULL) &&
SUCCEEDED(m_pObject->QueryInterface(
IID_IConnectionPointContainer
,
(LPVOID*)&pConnPtCont)))
{
ASSERT(pConnPtCont != NULL);
LPCONNECTIONPOINT pConnPt = NULL;
DWORD dwCookie = 0;
if (SUCCEEDED(pConnPtCont->FindConnectionPoint(iid, &pConnPt)))
{
ASSERT(pConnPt != NULL);
pConnPt->Advise(punkSink, &dwCookie);
pConnPt->Release();
}
pConnPtCont->Release();
return dwCookie;
}
return 0;
}
[/quote]
这个函数查找连接点并注册接口。至此,容器可以接受回调了:
查看COleControlSite::XEventSink::QueryInterface,支持三个接口:IID_IUnknown,IID_IDispatch,pThis->m_iidEvents
这个内嵌类是派发的源头,看看它的Invoke函数就明白了:
[quote]
STDMETHODIMP COleControlSite::XEventSink::Invoke(
DISPID dispid, REFIID, LCID, unsigned short wFlags,
DISPPARAMS* pDispParams, VARIANT* pvarResult,
EXCEPINFO* pExcepInfo, unsigned int* puArgError)
{
UNUSED(wFlags);
METHOD_PROLOGUE_EX(COleControlSite, EventSink)
ASSERT(pThis->m_pCtrlCont != NULL);
ASSERT(pThis->m_pCtrlCont->m_pWnd != NULL);
ASSERT(wFlags == DISPATCH_METHOD);
AFX_EVENT event(AFX_EVENT::event, dispid, pDispParams, pExcepInfo,
puArgError);
pThis->OnEvent(&event);
if (pvarResult != NULL)
::VariantClear(pvarResult);
return event.m_hResult;
}
[/quote]
Invoke函数什么也没做,只是调用了OnEvent:
[quote]
BOOL COleControlSite::OnEvent(AFX_EVENT* pEvent)
{
// If this control has a proxy CWnd, look for a matching ON_*_REFLECT
// entry for this event in its event map.
if ((m_pWndCtrl != NULL) &&
m_pWndCtrl->OnCmdMsg(m_nID, CN_EVENT, pEvent, NULL))
{
return TRUE;
}
// Proxy CWnd isn't interested, so pass the event along to the container.
return m_pCtrlCont->m_pWnd->OnCmdMsg(m_nID, CN_EVENT, pEvent, NULL);
}
[/quote]
进入消息循环,CCmdTarget::OnCmdMsg开头处中有如下内容:
[quote]
#ifndef _AFX_NO_OCC_SUPPORT
// OLE control events are a special case
if (nCode == CN_EVENT)
{
ASSERT(afxOccManager != NULL);
return afxOccManager->OnEvent(this, nID, (AFX_EVENT*)pExtra, pHandlerInfo);
}
#endif // !_AFX_NO_OCC_SUPPORT
[/quote]
跳到CCmdTarget::OnEvent来执行,该函数首先获得event表格的入口:
const AFX_EVENTSINKMAP_ENTRY* pEntry = GetEventSinkEntry(idCtrl, pEvent);
然后分析事件类型,如果事件类型为AFX_EVENT::event 会发生如下调用:
hResult = CallMemberFunc(&pEntry->dispEntry, DISPATCH_METHOD, &var,
(bRange ? &dispparams : pEvent->m_pDispParams), &uArgError);
在CallMemberFunc中整理参数,并且调用了事件的handler。
通过事件终于回调了相应函数。
要是让我们从头一行一行去写太麻烦了。。。MFC做了很多工作。为了用ActiveX,一个容器要实现的东西都有很多,户sir《课件17 ActiveX开发与使用》:
一个容器常常实现如下的接口:IOleClientSite(OLE容器使用的基本接口)、IOleInPlaceSite(支持在位激活的基本接口)、IOleControlSite(ActiveX控件场所所用的基本接口),IDispatch(捕获控件激发的事件)、IPropertyNotifySink(允许控制通知容器所做的属性修改并在修改之前请求许可)。编写一个控件容器不但要实现这些接口,还得考虑协议问题。例如,在控件创建之后,它和容器之间就要进行会话。首先它们将交换接口指针,容器将IDispatch和IPropertyNotifySink接口指针插入控件的连接点,容器通过调用IOleObject::DoVerb方法来激活控件。在某些情况下,容器会从控件中读取类型信息以便确定当通过它的IDispatch接口激发事件时自己要做什么工作。
有了MFC和强大的编译器,一切变得简单。建ActiveX,编译器会自动给出事件的“源”。“源”是个分发接口(dispinterface),回调函数的原型都在里面。在对话框中加入一个事件,对话框编辑器会自动查找控件事件的类型信息。类向导会用正确的签名插入一个函数。当“源”产生了某个事件时,会通过已注册的指针来调用IDispatch的Invoke函数,这样MFC就有机会通过改写Invoke像处理消息那样来分发事件。ActiveX为何能这么active??
再谈IDispatch
《COM技术内幕》中的有关论述:
一个自动化服务器实际上就是一个实现了IDispatch接口的组件,而一个自动化控制器则是一个通过IDispatch接口同自动化服务器进行通信的客户。自动化控制器不会直接调用自动化服务器实现的那些函数,而是通过IDispatch接口中的成员函数实现对服务器中函数的间接调用。
记得户sir曾让我们写过一个类,说通过传入函数名称,来调用这个类的函数。当时没明白老师的用心良苦。其实老师让做的就是模拟IDispatch接口。
windows程序设计课件16中有个测试例子:
Dim Obj
Dim Sum
Set Obj = CreateObject("SIMPLEPLUSDLL.FirstObj")
Sum = Obj.Add(100,200)
MsgBox Sum
Set Obj = Nothing
脚本中调用函数Obj.Add(100,200),就是用IDispatch调用的。有了调度接口,解释器不必明白这个类是怎么回事就可以调用函数。IDispatch本来就是为解释性语言设置的。不再需要这个类的信息了(如C++中头文件之类的),所以ActiveX能这么active啊。
从另一方面讲,IDispatch模拟了C++通过虚函数表来调用函数。C++中的一次调用:
p->FuncA("test");将被解释为:(*(p->pvtbl[IndexOfFuncA]))(p, "test");
编译器会到虚函数表中通过索引来取得函数指针,然后调用虚函数。类的非static成员函数会将this指针也当参数传进去。同样调度接口也是通过显式的DISPID来调用函数的。一般来说,这个ID相当于虚函数表中的索引。毕竟C++中COM接口是一个指向一个函数指针数组的指针,此数组的前三个元素分别是QueryInterface,AddRef以及Release。另外像MFC处理事件也是通过Invoke函数把事件当消息来分发。IDispatch使我们可以自由决定如何调度函数。Don Box所说“COM是一种更好的C++”,我想在IDispatch也有体现。
再谈套间
想到套间是因为ActiveX一般是有窗口的,原来读书时对那个“套间线程”和“自由线程”感到很迷惑--ActiveX应该是存在于STA中,因为STA适合于用户界面线程。在看MFC对ActiveX的封装时偶然看到过消息过滤器类,而消息过滤器是STA特有的。套间不同于线程,一个线程可以多次调用CoInitialize和CoUnInitialize出入多个套间,套间更是种逻辑的概念,线程更像个函数。我觉得套间是更细粒度的单位,来控制并发访问。如果是STA,我们根本不用去管线程同步的问题,真方便啊。
我想到了操作系统的发展。
不说最原始的job了。由进程到线程的演变是很大的进步。这两个概念可以划分清楚:进程是资源分配和管理的单位,线程是调度执行的单位。在线程的基础上又发展出套间的概念。套间基于对象的线程模型,定义了一组对象的逻辑组合,这些对象共享同一组并发性和重入限制。跨套间的访问均需要列集(我曾对同一进程中传个指针还要列集一下非常不解>_
[quote]
The following example is a service process that supports only one service. It takes two parameters: a string that can contain one formatted output character and a numeric value to be used as the formatted character. The SvcDebugOut function prints informational messages and errors to the debugger.
SERVICE_STATUS MyServiceStatus;
SERVICE_STATUS_HANDLE MyServiceStatusHandle;
VOID MyServiceStart (DWORD argc, LPTSTR *argv);
VOID MyServiceCtrlHandler (DWORD opcode);
DWORD MyServiceInitialization (DWORD argc, LPTSTR *argv,
DWORD *specificError);
VOID _CRTAPI1 main( )
{
SERVICE_TABLE_ENTRY DispatchTable[] =
{
{ TEXT("MyService"), MyServiceStart },
{ NULL, NULL }
};
if (!StartServiceCtrlDispatcher( DispatchTable))
{
SvcDebugOut(" [MY_SERVICE] StartServiceCtrlDispatcher error =
%d\n", GetLastError());
}
}
VOID SvcDebugOut(LPSTR String, DWORD Status)
{
CHAR Buffer[1024];
if (strlen(String)
[/quote]
这段例子程序是VC6自带的MSDN上找到的。我没编译过--从该程序大体看出服务是怎么回事。
要成为服务,需要在注册表里HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services下添加注册项。
应用程序中可调用API函数CreateService来完成注册,这个函数的各个参数正好解释了注册表中的各个键值,详情见MSDN。注册好后,根据Start键值不同,有的服务是开机时启动,有的是按需启动。主线程启动后,会调用StartServiceCtrlDispatcher,该函数不会返回,一直到所有请求引发的服务线程结束。 传给该函数的是分发表,如果有请求到来,会从表中查找服务名称对应的函数,SCM会启动新的线程运行该函数。这个函数中要调用RegisterServiceCtrlHandler 注册命令的handler,来处理SCM发给它的控制命令和自定义命令。
服务线程的函数原型是
[quote]
VOID WINAPI ServiceMain(
DWORD dwArgc, // number of arguments
LPTSTR *lpszArgv // array of argument string pointers
);
[/quote]
control handler的函数原型是
[quote]
VOID WINAPI Handler(
DWORD fdwControl // requested control code
);
[/quote]
下图很好的表示了服务进程做的事情:

当然到了.net,编写服务都是用封装好的类了。
windows中的任务和服务有所不同,编程上前者是依靠COM接口来创建,调度的。通过ITaskScheduler接口创建或激活Task Object,得到ITask接口指针来配置任务。2003的MSDN上有关Task的例子全部是C++写的,可能因为编写任务不像编写服务那么麻烦,所以没封装到.net类库。task也好,service也好,只不过是特殊的process而已。再谈MTS
MTS是建立在COM基础上run time environment。数据库原理的教材上光讲原理了,看了MTS,就会对分布式事务有直观的认识。J2EE还没学,不懂JTA。倒是EJB,感觉和让MTS托管的对象很类似。
先贴MSDN上和MTS有关的名词:
[quote]
context:
State that is implicitly associated with a given Microsoft Transaction Server object. Context contains information about the object's execution environment, such as the identity of the object's creator and, optionally, the transaction encompassing the work of the object. An object's context is similar in concept to the process context that an operating system maintains for an executing program. The Microsoft Transaction Server run-time environment manages a context for each object.
activity:
A collection of Microsoft Transaction Server objects that has a single distributed logical thread of execution. Every Microsoft Transaction Server object belongs to one activity.
role:
A symbolic name that defines a class of users for a set of components. Each role defines which users are allowed to invoke interfaces on a component.
resource dispenser:
A service that provides the synchronization and management of nondurable resources within a process, providing for simple and efficient sharing by Microsoft Transaction Server objects. For example, the ODBC resource dispenser manages pools of database connections.
stateless object:
An object that doesn't hold private state accumulated from the execution of one or more client calls.
server process
A process that hosts Microsoft Transaction Server components.
A Microsoft Transaction Server component can be loaded into a surrogate server process, either on the client's computer or into a client application process.
[/quote]
MSDN上讲这一部分时一再强调组件要封装业务逻辑(business logic),所谓stateless, 只要提供服务就不要保存什么状态,服务完了这个组件就可以滚蛋了。这里说的组件是三层结构中第二层的组件。原来的client/server结构演化成为客户,服务端,数据端,三层结构。服务端很大程度上不再需要保存自身的状态,只需负责客户与数据端的联系即可。而出于安全性,鲁棒性,数据一致性,控制并发访问等要求,状态需要被统一管理。要么保存到数据库中,要么通过SharedPropertyGroupManager。使用它来创建或获得SharedPropertyGroup,在属性组中通过属性名来访问属性。虽然MSDN极力建议要建立“stateless object”,但有时还是要保存下状态。状态就是属性。统一管理属性的好处是,使用Shared Property Manager使得在多用户的环境中使用共享属性更加容易。为什么不把属性作为组件类的成员变量?如果那样做,在组件为一个进程提供服务的过程中,组件必须在内存persistent,需要额外的代码:
1 Referencing all user instances to the object.
2 Maintaining a locking mechanism to prevent concurrent access to the object
而且stateful object在激活时要提供初始化代码,反激活(deactivation)时不会继续保存成员变量的值。这样就必须继承IObjectControl来控制激活和反激活。组件有激活和非激活态的好处是,在组件没被使用但还有外部引用时(此时不能把组件删除掉),可以让组件处于非激活状态,暂时释放资源。当调用非激活状态的对象的一个方法时,客户的引用会自动绑定到新的对象上,这个对象应该是对象池中已有的,池中没有的话会重新创建。MSDN上对pooling的解释:
A performance optimization based on using collections of pre-allocated resources, such as objects or database connections. Pooling results in more efficient resource allocation.
当然自己写的组件通过IObjectControl可以告诉MTS运行时该组件不能pooling。
说到重新绑定到对象,如果是在一个事务中,对象的上下文会和事务的上下文联系起来。我不知道这个上下文具体是什么东西,反正对象通过接口可以知道自己是不是运行在事务里,以及调用方法的role是什么,可以通过SetComplete和SetAbort告知自己的运行情况。SetComplete指自己工作完毕了,可以被反激活。SetAbort是相反的意思,而且整个事务都会被abort。。。组件安全性主要体现在配置角色(role),通过上下文可以知道是不是特定角色在使用自己。这个角色是基于package的。一个package中的组件会运行在同一个服务进程中。这些包啊角色啊都是通过MTS自带工具管理组件时配置的。说到ObjectContext,提一下IIS,先看MSDN上一图:

中间部分显示DCOM是使用MTS components的standard transport。左侧单独列出HTTP通过IIS访问MTS,可以使用ObjectContext取得IIS内置对象:
Request
Response
Server
Application
Session
例子如下:
Dim oc As ObjectContext
Dim str As String
…
Set oc = GetObjectContext()
oc("Response").Write "" & str & ""
MSDN讲MTS时很多例子是用VB写的--,不只是和网络有关的部分。C++写太麻烦了,查询这接口那接口的。由于我对DCOM及IIS的内部原理不很了解,在此不多说了。反正对象上下文用处很大。
MTS带给我全新的资源理念。
什么是resources?传统意义上操作系统关心的资源有CPU,内存等。MTS中的资源是用户定义的。对象就可算成资源,这正是组件要封装业务逻辑(business logic)来提供服务的理念,应用程序是来使用对象的服务的。
[quote]
A resource is anything a resource dispenser creates. For example, a connection to a resource manager is a common resource.
Resources reside in the resource dispenser's memory; they are never copied to DispMan. A resource is known only by an opaque handle (RESID). Resources may or may not be capable of performing transactions.
[/quote]

DispMan管理各种对象,表示与数据库连接的对象,网络连接对象,用户自己写的对象等等。操作系统才不会管你那块内存是什么东西来着。可以看出MTS增加了操作系统的层次。用户在MTS之上建立的应用程序,往下看恐怕大部分只看到MTS。到了.net framework,更是如此。我们会说我们的程序建立在.net框架上而不是某个操作系统之上。从WIN2K开始确实要变成微内核了。
上图右侧的dispensers通过API接口来提供服务。通过ODBC访问数据库时调用的函数都是组件暴露出来的API函数。
dispensers负责实现IDispenserDriver接口,通过注册自己(通过IDispenserManager注册)来获得相应的IHolder指针,DispMan会创建个Holder对象,返回指向它的指针给IDispenserDriver。通过IHolder来创建资源。当调用IHolder::AllocResource 时,首先会去对象池中去找有没有可用的。找不到的话IHolder会回调IDispenserDriver::CreateResource。对象池是个逻辑上的概念,是由DispMan来管理的。
oracle和DB2都是内建的对分布式事务的支持,我没听说SQLServer分布式事务是built-in的。MTS是适配器,让开发人员可以在SQLServer平台中做分布式事务。
闲扯下数据库生产商三巨头:Oracle, Microsoft,IBM
先说Oracle
《数据库系统概念》讲Oracle时特别强调了其并行执行的能力,SMP上啊集群上啊用起来想必很爽。Oracle数据库方面对网格计算投入很多。为什么做到这样?我想IBM有大型机,Microsoft有操作系统,而Oracle只有Oracle。大型机上可能用非DB2的系统吗?于是甲骨文就专心研究集群啊网格啊,为匹兹堡计算中心提供网格产品。。不管怎么说,我们的Oracle还是很强的。穆老师上课的时候最经常举的例子就是——Oracle
扯扯Microsoft
微软精力很旺盛。我们4年学的原理课除了计算机原理没微软的东西外,其他都有。。操作系统原理,数据库原理,编译原理。。《数据库系统概念》说那两个数据库对XML的支持时一笔带过,唯独说到SQLServer时用了3页半纸--没办法,Microsoft对XML情有独钟啊,本文第四部分说.net时还会提到。据说下一版本的SQLServer支持用C#等框架支持的语言来编写存储过程。还有以后Office的宏也可用.net支持的语言编写。总之,微软要在整个软件行业称霸。总是有反对MS的声音。黑客热衷攻击WINDOWS,我想不开源漏洞都这么多,开源了岂不要死人。官司也打了不少,欧盟坚持认为操作系统不应该自带媒体播放器,说是垄断。以后连浏览器也别带了。比尔•盖茨却回答:
如果没有捆绑销售的话,你能想象出Windows现在是个什么样子吗?有些人希望能以100美元的价格向消费者推销TCP/IP堆栈,但我们在系统中向用户免费提供了TCP/IP协议,这就难怪这些人要攻击我们了。
再扯IBM
IBM和Microsoft一样,兴趣很广泛。从硬件到软件,从编译器到数据库,什么都做 >_
[color=blue]
追忆似水年华
[/color]
说完COM,不禁要追忆下一年前的峥嵘岁月。军训是在本部。每天的训练结束后,我都会跑到图书馆小钢楼里,一边喝水一边看书。正是那时看完了讲C++的几本书,中国电力出版社的红皮的,很不错。记得有本叫《C++并行与分布式编程》,丫的当时看太难了,没看下去>_
可是怎么就走到这里了呢。回首来处,也无风雨也无晴。
[b]
[color=#ff0000]4. .net——千呼万唤始出来,犹抱琵琶半遮面
[/color]
[/b]有美玉于斯。韫椟而藏诸。求善贾而沽诸 ——《论语•子罕》
吾生也有涯,而知也无涯……臣之所好者道也,进乎技矣……三年之后,未尝见全牛 ——《庄子•养生主》
采薇采薇,薇亦刚止。曰归曰归,岁亦阳止 ——《诗经•小雅•采薇》
大二下学期我才接触.net,是个初学者。经过几个月的学习,我觉得用VS.net写程序很简单,C#也比C++好用。.net的内涵不仅仅是方便人们的编程,而是带来新的理念。
将近10年前Don Box总结的面向对象的三次浪潮:
实现继承,基于接口的软件开发,有效管理对象。
第一次浪潮就是传统的C++开发;第二次浪潮的代表产物是COM,将接口与实现分离,接口构造出抽象请求的模型,而对象则是由这些请求组成的;第三次浪潮代表产物有:通过MTS管理的对象,基本思想是从逻辑上看一个对象是状态和行为组合在一起的模型,但从物理实现上要把他们明确分开,让MTS来管理对象的状态,开发人员就可充分利用MTS对“并发性和锁的管理,错误恢复,数据一致性和细粒度的访问控制”的支持。可惜的是显式使用MTS的开发人员并不多。.net符合“有效管理对象”的要求。从Java开始对象就是在托管堆上建立的,.net垃圾收集和Java一样也采用跟踪,而不是COM用的引用计数,好处是不必担心循环引用,且通过除去 COM AddRef 和 Release 机制,性能得到进一步提高,并且对象所需的内存更少。这就可看作对状态的管理,状态就是类的实例成员变量。自我描述也是管理状态的机制。Java通过java.lang.reflect来实现反射。.net则更近一步,定制属性的出现增强了类描述自己的能力,就像public等放在名称前的词,描述了类,属性,方法的信息。C#号称“一切语言元素皆为对象”,确实很OO。又扯远了。。最近刚开始学J2EE,对第三次浪潮有了新的理解——EJB。Session Beans最好是stateless的,Entity Beans可以让容器来管理自己的状态(Container-Managed Persistence),这些都和MTS中强调的一致。容器的作用是:Management of component Lifecycle,Transaction management,Security checks,Resource pooling,State management。俺还没学到EJB及其竞争对手COM+,不多说了。还是在老金课上第一次听说Java程序是运行在容器中的而不是JVM上的--只学过J2SE,确实没感觉到。
先来看看.net架构:

最上层是语言。C#是像VB一样简单,像C++一样强大的新语言,由 Turbo Pascal, Delphi, and Visual J++的首席设计师Anders Hejlsberg 倾心三年设计。C#中重载运算符要比C++简单明了。is,as运算符简化了类型检查和转换。直接支持属性,事件和attributes属性。想想Java类库中许多方法名前带set/get,.net语法中就直接有属性,多方便啊,事件也是语法层面上直接支持的。感觉普通的类和组件的差距减小了,COM组件中有属性和事件的概念,IDL有关键字来描述属性,事件则是通过连接点机制,写起来够麻烦的,见本文第三部分对MFC封装事件的解析。而元数据里的类信息使得类可以自我描述,丫的原来只见过VB中用ActiveX时拖到编译器上会“自我描述”,把属性暴露给编译器。.net时代,真是进化了不少啊。J#也是.net的重要组成部分。微软宣称它的JVM是最好的,至少在WINDOWS上。不过Java产品市场还是被IBM和Sun占有--但要想将Java定位成行业中唯一的语言,那是相当愚蠢的做法。于是,有个名为“JUMP to .NET”的标题全称是:Java Users Migration Path to .NET。.net给Java 程序员提供开发Web Services的开发环境,J++和J#可以说是微软版的Java,C#则是一种新式武器。不可否认.net框架学了Java很多东西,有些关键字如final等就来自Java,还有单根体系,强类型,以及将文件编译成二进制码而不是可执行码等。.net框架中间一层表示的XML是重要的部分。“XML is an industry standard and there currently is an XML Kernel in all Microsoft products.”微软要贯彻“Software As A Service”的理念。XML能在不同平台间交换信息,真正的与平台无关,而且使信息能“自我描述”。 正是这样一种角色吸引了一向鄙弃Java的微软公司,积极参与XML标准的制定,开发了用于应用集成和电子商务的XML框架——BizTalk,目标是推动XML的迅速普及和应用。XML和CLS都是行业标准, .net架构是基于标准开放的架构。
扯扯MCMS。这也是个高深的东西,虽然看着MSDN上的例子编程序不是难事,本文这一部分纯粹靠本人的想象在胡扯。。对MCMS感兴趣,我觉得它体现的正是“Software As A Service”:

MCMS确实简化了网站的维护。更有利于专业分工精细化。一个网站可以通过XML和IDL的混和产物WSDL来发布服务--所以卖车的网站不用自己去跟踪最新的车型,只要消费汽车生产商的网站提供的服务就可以了。而MCMS保证数据会及时更新,数据一致性,安全性等,并且能记帐。让我想到Google和美联社的合作,美联社说你搜索到我的新闻,应该付给我钱,新闻是我雇记者写的。这就可以通过记帐来收费。让美联社把自己网站上的新闻作为服务提供给Google。MCMS就是这样一种平台,可以让一个网站做生产者,一个网站做消费者。但搜索不是转载,本身是个自动过程,并且不需要时效性很强,这里用Google做例子只是说明可以发布服务。而微软的野心不仅仅是online service,如上图所示,通过MCMS发布的信息可以被桌面应用,手持设备,网络应用等等来消费。看到个啼笑皆非的广告,苏宁电器用GPS定位来送电器,丫的又不是发射导弹,用的着GPS么。。。我联想到了交通管理,不论是不是去送货的汽车,都安上智能手机,交通局数据中心统一发布路段信息,而汽车上的智能手机就能够获得及时的消息确定走哪条路不堵车。全国的城市都可这么做,一辆外地车到了新的城市,不用买地图就可获得地图服务,而数据的内容可以远远多于路段情况,还可以标注出哪里有旅馆,哪里有停车场,以及旅馆的入住情况,当然这可能要消费卫生管理部门的信息,因为旅馆是卫生部门来管理的,需要该部门通过MCMS发布旅馆信息,交通部门不用去管旅馆信息的更新,只需消费就可以了。一篇有关供应链管理(SCM)的论文中写到“目前国内连锁企业SCM系统的现状是:各连锁企业门店不同程度实现了基于Intranet的POS/MIS系统,但门店和门店之间、门店和总部之间,总部和供应商之间基本上还处于信息孤岛状态。无法实现信息共享”。原来为了实现共享信息,各分店的MIS每天固定时间给总部发送加密的邮件。我想可以考虑使用MCMS来实现各个MIS系统的整合,大家都发布信息让有权限的人访问,这样信息是分布式维护的,及时更新的,而各个分店也可共享。
“.net为未来十年做好了准备,而你呢?”距离提出这个口号已经过去N年。以后的路该怎么走?NSObject学长的话很有道理:So whatever you choose, keep one thing in mind, never stop thinking, and you will finally find something worth doing, in your unique way.世上本没有路,走的人多了也就成了路。
行程虽艰辛却不会孤独,下一站的路口必将有人与你殊途同归。
那么让我们一起继续走下去吧。
长风破浪会有时,直挂云帆济沧海:

面向对象以前的年代
学过汇编,C,VHDL,JCL。JCL不算编程语言,只是种脚本罢了。VHDL这种帕斯卡和Ada的结合产物只学了一点,让我明白语言不仅仅是编程或编网页用的。VHDL产生的不是可执行文件,产生的是表示硬件的图形。上了体系结构课才知这种语言归类为“应用语言”,同属一类的还有SQL。。大二下对机器人感过兴趣,曾有研究Ada的冲动,可听牛及S君说工控机编程是用C/C++,于是作罢,只记住了这个美丽的名字。
后记
荀子讥墨子“蔽于用而不知文”,看他的文章倒是铺陈排叙比兴来一大堆杂七杂八的东西,可说的道理往往简单明了。写本文的初衷在于用而不在文,就不多修饰了。还是他祖师爷说的好,“君子欲讷于言而敏于行”。
本文主要写了两年所学的种种编程语言,欲以究天人之际,通语言之演变。至于参考书目,就那几本耳熟能详的经典著作,《COM本质论》等等,还有MSDN。引用了各书中的许多经典论述,小子不敏,悉论先人所次旧闻。古人评价“北人看书如显处视月,南人学问如牖中窥日”。希望偶们软院同胞不论北人南人不论身处显处牖中,闲暇时多读点儿书,多读懂点儿书,求思之深而无不在,不要畏难而退。
罗嗦了这么多,如有纰漏还望回帖指正,所谓“言者所以在意,得意而忘言”。
学一门编程语言就像认识一个人。有感于小狼MM“花事了”:
汇编 —— 古调虽自爱
C —— 竹露滴清响
C++ —— 浊酒尽余欢
Java —— 我歌月徘徊
COM —— 一江琉璃碧
C# —— 江南烟水路
VB/VB.net ——长乐未央时
JCL,VHDL ——今夕复何夕
各种脚本语言——隔墙秋千影
大一上的我——莲子青如水
大一下的我——落花人独立
大二上的我——大漠孤烟直
大二下的我——梦中不识路
最近评论