CAR构件远程调用中服务器端线程池模型的研究与应用
2008-07-02
作者:刘振国,陈榕,张爱莲
摘 要: CAR构件进行远程调用" title="远程调用">远程调用的原理和过程,针对该过程中服务器端的效率问题提出了进程级线程池解决办法,构建了进程级线程池模型,实现了关键部分的代码。最后结合一个简单的构件远程服务示例说明了其工作过程。
关键词: CAR构件 进程级线程池 远程调用
20世纪80年代以来,目标指向型软件编程技术有了很大的发展,为大规模的软件协同开发以及软件标准化、软件共享、软件运行安全机制等提供了理论基础。其发展可以大致分为面向对象" title="面向对象">面向对象编程、面向构件编程、面向中间件编程三个阶段。
CAR技术就是总结面向对象编程、面向构件编程技术的发展历史和经验,为更好地支持面向以Web服务为代表的下一代网络应用软件开发而发明的。CAR为构件软件和应用程序之间进行通信提供了统一的标准,它为构件程序提供了一个面向对象的活动环境。
1 CAR构件技术及CAR远程构件调用的基本原理
CAR(Component Application Run-Time)是一个具有国内自主知识产权的构件系统,由上海科泰世纪科技有限公司开发。CAR构件技术定义了一套网络编程时代的构件编程模型和编程规范,它规定了一组构件间相互调用的标准,使得二进制构件能够自描述及在运行时动态链接。
1.1 CAR远程接口自动列集/散集技术
CAR是面向对象的软件模型,对象是它的基本要素之一。使用对象的应用(或另一个对象)称为客户,有时也称为对象的用户。对象和客户之间的相互作用建立在客户/服务器模型的基础上。
当客户端" title="客户端">客户端和服务器端所在地址空间不同时,客户端的进程要调用服务器端的构件服务,属于远程构件调用。CAR构件技术支持远程接口调用,通过调用数据的列集/散集技术进行不同地址空间的数据交互。构件服务和构件服务调用者可以处于操作系统的不同空间,而调用者可以如同在同一地址空间中使用构件一样透明地进行远程接口调用,也就是说完全向用户屏蔽了底层使用标准的列集/散集过程。
CAR的自动列集/散集主要用于和欣操作系统(Elastos),它在构件的调用过程中的地位类似于COM的自动列集/散集。用户如果采用默认的列集/散集过程,则使用一个远程接口如同使用一个本地接口一样,完全屏蔽了数据的交换、传递过程。
Elastos2.0以存根/代理机制来实现远程接口自动列集/散集,主要涉及到三个对象,即处于客户端的代理(Proxy)对象,处于服务端的存根(stub)对象,以及处于内核的(Object)对象。
一个客户端进程不一定只调用一个远程构件服务。为了更方便有效地与各个远程构件交互数据,Elastos在客户端为每一个对应的远程服务建立一个代理对象,记录客户进程的信息、远程服务构件对象的信息以及调用状态等,负责客户进程与对应的远程服务联系。
Elastos会为每个提供远程服务的构件对象建立一个存根对象,客户端代理不是直接与远程提供服务的构件对象联系,而是与存根对象联系,通过存根对象来调用构件对象。
内核Object对象是联系客户端和服务器端的枢纽,保持了相关服务的信息以及创建对象代理所需要的一些信息。它的建立标志着用户可以通过某种方式远程获得相关服务(服务的发布)。
CAR的自动列集/散集通过在程序运行过程中动态生成存根/代理实现。一个用户的远程构件调用首先通过代理对象转发到内核相应的Object对象,Object对象寻找到相应的服务进程以及存根对象,启动某一个服务线程并将调用转发给存根对象,然后再由存根对象去完成调用构件方法的过程。而调用返回的过程正好与这个流程相反。图1是对象流程调用的一个简单示意图。
1.2 客户端调用远程服务步骤
当确定代理、存根都存在,并且从客户端到服务器端建立一条可以相互通信的通路后,客户端就可以开始远程调用了。其流程如下:
(1)客户端用户的一次CAR远程调用会转发到代理对象上,代理对象将调用栈中的数据根据元数据" title="元数据">元数据信息打包,并传给内核;
(2)内核根据注册信息找到服务及存根对象,并将打包的信息传给存根对象;
(3)存根对象根据元数据将数据解包,并构建和客户端调用栈相应的栈内数据,并调用真正的构件服务接口函数" title="接口函数">接口函数;
(4)服务构件接口函数完成调用,并返回;
(5)存根对象获取接口函数调用的参数信息及返回信息, 并将其打包,通过系统调用返回到内核;
(6)内核将服务器端的返回信息传递给客户端,客户端从系统调用返回;
(7)代理对象获得返回信息,根据元数据解包并回填到用户调用栈中。整个远程构件方法调用过程完成。
图2是按照数据流程的形式给出的一个进程间远程调用的全部过程。
从图2可以看出,在服务进程内,服务最终是通过服务线程来提供的。很明显,上述过程可能存在一个问题:一个进程可能提供不止一种服务,所以可能在服务进程内需要多次重复地创建服务线程来为客户端服务。线程的创建、销毁和调度本身是有代价的,如果一个线程的任务相对简单,则时间和空间开销便不容忽视;如果一个服务进程同时对外提供多个服务,这种开销会显著地降低服务效率,从而导致整个系统的效率降低。对于Elastos这样的嵌入式实时操作系统来说,这是非常致命的。
2 进程级线程池模型
由上面的分析可以看出,问题的关键在于对线程资源的低效管理。对于共享资源,有一个很著名的设计模式:资源池(Resource Pool)。该模式正是为了解决资源的频繁分配﹑释放所造成的问题。为解决上述问题,可以采用线程池技术,这里的线程池基于进程级。
2.1 线程池技术的原理
线程池采用预创建技术,在应用程序启动之后,将立即创建一定数量的线程(N),并放入空闲队列中。这些线程都是处于阻塞(Suspended)状态,不消耗CPU,但占用较小的内存空间。当任务到来后,缓冲池选择一个空闲线程,把任务传入此线程中运行。当N个线程都在处理任务时,缓冲池便自动创建一定数量的新线程,用于处理更多的任务。在任务执行完毕后,线程也不退出,而是继续保持在池中等待下一次任务。当系统比较空闲时,大部分线程一直处于暂停状态,线程池自动销毁一部分线程,回收系统资源。
基于这种预创建技术,线程池将线程创建和销毁本身所带来的开销分摊到了各个具体任务上,执行次数越多,每个任务所分担到的线程本身开销则越小。
面向对象编程中,创建和销毁对象很费时间,因为创建一个对象要获取内存资源或者其他更多资源。所以提高服务程序效率的一个手段就是尽可能减少创建和销毁对象的次数,特别是一些耗费大量资源的对象创建和销毁。如何利用已有对象来服务是一个需要解决的关键问题,也是为什么要设计线程池的原因。
针对线程的特点,可将一般的线程使用过程的时间分段为:
T1——创建线程的时间
T2——在线程中执行任务的时间
T3——线程销毁的时间
则完成一个任务所需时间T=T1+T2+T3。
对于Elastos中完成ezAPI调用的线程而言,T2所占用的时间相对于T1、T3较短,T2中包含的时间仅为一个接口函数执行的时间。可以看出,线程本身的开销所占的比例为(T1+T3)/(T1+T2+T3)。如果线程执行的时间很短,则这笔开销可能占到20%-50%左右;如果任务执行时间很频繁,则这笔开销将不可忽略。所以线程池在Elastos操作系统中的使用,理论上可以显著改善系统性能。图3是其简单的原理图。
从图中可以看出,线程池中的线程(池线程)同普通的线程无异,实现上属于同类数据结构。但是对于线程池中的线程,其所需资源已经分配并初始化,池线程只用在栈中构建好ezAPI调用的相关栈布局,就可以直接调用了。
2.2 进程级线程池模型的实现
在模型实现过程中,主要涉及三个类:进程类、线程类、线程池类。线程池对象作为进程数据结构的一部分而存在,一个进程对应一个线程池,每个线程池中存在0~MaxThreadNum个线程。当进程初创建时,线程池中的线程数为0。
2.2.1 线程池类定义
class ThreadPool {
public:
void Initialize(CProcess*pOwner);
ECODE GetThread(Thread**ppThread,ScheduleClass*pScheduleClass,uint_t uSchedulePolicy);//获取线程池中线程
void PutBackThread(Thread*pThread);//将执行完任务的
//线程回收
inline Thread*FindThread(ScheduleClass*pScheduleClass,
uint_t uSchedulePolicy);//寻找适合线程
inline void SetCapacity(UINT uCapacity);//设置线程池容量
~ThreadPool( );
private:
ECODE CreateThread(Thread**ppThread,ScheduleClass?鄢pScheduleClass,uint_t uSchedulePolicy);//在线程池中创建
//新线程
void DestroyThread(Thread*pThread);
public:
CProcess* m_pOwner;//线程池所在的进程对象
UINT m_uCapacity;//线程池容量
DLinkNode m_threadList;//线程池中的空闲线程队列
UINT m_cThreads;//线程池中的空闲线程数量
KMutex m_threadLock;//线程池互斥变量,用于同步
};
2.2.2 进程类中有关线程池的代码
class CProcess:
{
public:
CARAPI_(ULONG) AddRef(void);
CARAPI_(ULONG) Release(void);
CARAPI Start(/*[in]*/ WString wsName, /*[in]*/ WString wsArgs);
CARAPI Kill( );
public:
ThreadPool m_threadPool;//每个进程均有一个线程池
ProcessHContext m_hcontext;
};
2.2.3 进程类与线程类中关于线程池操作部分的代码
INLINE ECODE CProcess∷GetThread(Thread**ppThread)
//获得线程
{
if(ProcessState_Finished==m_processState && this !=∷GetCurrentProcess()) {
return E_PROCESS_ALREADY_EXITED;
}
return m_threadPool.GetThread(ppThread,pScheduleClass,uSchedulePolicy);
}
ULONG Thread∷Release(void) //销毁线程
{
if (ThreadState_Running!=m_uPoolThreadState) {
if (1==lRef && !this->IsFinished() && m_pOwner)
m_pOwner->m_threadPool.PutBackThread(this);
else if (0==lRef) {
m_pOwner->m_threadPool.m_threadLock.Lock();
m_inProcLink.Detach();
m_pOwner->m_threadPool.m_threadLock.Unlock();
}
delete this;
}
else assert(0 !=lRef);
return (ULONG)lRef;
}
使用上面描述的线程池模型后,进程间远程调用的过程就发生了变化,内核空间部分的步骤7由原来的在服务进程中直接创建新的线程变成了从服务进程的线程池中直接获取一个空闲线程来提供服务,如图4所示。
3 线程池应用举例
下面以一个简单的远程调用实例说明上面所描述的线程池模型的应用。
假设现在有三个CAR构件:car1.dll、car2.dll、car3.dll,分别提供构件对象obj1、obj2、obj3,每个对象中实现接口函数如下:
obj1∷server1( )
{
printf(″this is the first service!\n″);
}
obj2∷server2( )
{
printf(″this is the second service!\n″);
}
obj3∷server3( )
{
printf(″this is the third service!\n″);
}
现有一个服务进程如下:
void main( )
{
IObject*svr1 = new obj1( );
IObject*svr2 = new obj2( );
IObject*svr3 = new obj3( );
EzRegisterService(server1,svr1);//注册服务1
EzRegisterService(server2,svr2);//注册服务2
EzRegisterService(server3,svr3);//注册服务3
wait( );
EzUnregisterService(server1);//注销服务1
EzUnregisterService(server2);//注销服务2
EzUnregisterService(server3);//注销服务3
svr1.delete( );
svr2.delete( );
svr3.delete( );
……
return;
}
该进程通过三个构件提供三种服务,每种服务提供一个简单的打印功能。
这时有三个客户进程分别如下:
void main( )
{
IObject*host1;
EzFindService(L″server1″,&host1);
//查询服务1
host1->server1();//提供服务1
……
return;
}
void main()
{
IObject*host2;
EzFindService(L″server2″,&host2);//查询服务2
host2->server2();//提供服务2
……
return;
}
void main()
{
IObject*host3;
EzFindService(L″server3″,&host3);//查询服务3
host3->server3();//提供服务3
……
return;
}
上述过程中,当三个客户进程并发进行时,服务进程就需要为每一种服务创建一个服务线程以提供服务,这时就会利用线程池来创建所用的线程。这就大大提高了服务效率,从而提高了整个系统的性能。
线程池致力于减少线程本身的开销对应用所产生的影响,但前提是线程本身开销与线程执行任务相比不可忽略。如果线程本身的开销相对于线程任务执行开销而言可以忽略不计,则此时线程池所带来的好处不明显。例如对于FTP服务器以及Telnet服务器,通常传送文件的时间较长,开销较大,则此时采用线程池未必是理想的方法,而可以选择“即时创建,即时销毁”的策略。
在构件远程调用过程中,服务进程中线程池的设计与实现可以大大提高服务效率。本文通过简要介绍CAR构件远程调用的原理,引出了该过程中存在的效率问题,然后针对问题提出了进程级线程池的模型,并给出了一个简单的实现,通过该线程池能够极大地提高构件远程调用中服务器端的服务效率。
该线程池是在进程级别实现的,并没有实现系统级线程池。为了在两种方案及典型的应用情况下取得最好的服务性能,还应该进一步实现系统级线程池,并根据典型应用制定测试用例,对两种方案进行效率上的比较,从而从中选取一种更优的解决方案。
参考文献
[1] KORETIDE.Elastos 2.0 Operating System Manual[M/CD].http://www.koretide.com.cn,2004/2005,6.
[2] KORETIDE.CAR′s Manual[M/CD].http://www.koretide.com.cn,2004/2005,6.
[3] BOVET D P,CESATI M.Understanding the Linux Kernel,2nd Edition[M].Sebastopol:O′Reilly,2002:22-35.
[4] BUTENHOF D R.Programming with POSIX Threads.AddisonWesley,1997.
[5] KRIEMANN R.Implementation and Usage of a Thread Pool based on POSIX Threads Max-Planck-Institute for Mathematics in the Sciences.2004-10-19
[6] CALCOTE J.Thread pools and server performance.Dr.Dobb′s Juurnal,1997,7:60-64.
[7] NICHOLS B,BUTTLAR D,FARRELL J P.Pthreads Programming.O′Reilly & Associates,Sebastopol,CA,1996.