sherrylso

C++博客 首页 新随笔 联系 聚合 管理
  18 Posts :: 0 Stories :: 124 Comments :: 0 Trackbacks

2011年2月18日 #

Java通过JNI机制调用c/c++写的native程序。c/c++开发的native程序需要遵循一定的JNI规范,下面的例子就是一个JNI函数声明:
JNIEXPORT jint JNICALL Java_jnitest_MyTest_test
  (JNIEnv 
* env, jobject obj, jint arg0);
JVM负责从Java Stack转入C/C++ Native Stack。当Java进入JNI调用,除了函数本身的参数(arg0),会多出两个参数:JNIEnv指针和jobject指针。
JNIEnv指针是JVM创建的,用于Native的c/c++方法操纵Java执行栈中的数据,比如Java Class, Java Method等。

首先,JNI对于JNIEnv的使用, 提供了两种语法: c语法以及c++语法,如下:
c语法:
jsize len = (*env)->GetArrayLength(env,array);
c++语法:
jsize len =env->GetArrayLength(array);
(注:由于C语言并不支持对象的概念,所以C语法中需要把env作为第一个参数传入,类似于C++的隐式参数this指针).


另外: JNIEnv有几个设计的原则:
第一、JNIEnv指针被设计成了Thread Local Storage(TLS)变量,也就是说每一个Thread, JNIEnv变量都有独立的Copy。这样做的原因主要是考虑到:
由于JVM要运行在多个平台(除了主流的Windows,Linux等平台),JNI内部实现很多要依赖到TLS, 为了减少对TLS的依赖,所有TLS based的数据都会存放于JNIEnv中。这样相当于只依赖一个TLS based的变量JNIEnv。由于JNIEnv指针是TLS的,所以你不能把Thead#1使用的JNIEnv传给Thread#2使用。

第二、JNIEnv中定义了一组函数指针,c/c++ Native程序是通过这些函数指针操纵Java数据。这样设计的好处是:你的c/c++ 程序不需要依赖任何函数库,或者DLL。由于JVM可能由不同的厂商实现,不同厂商有自己不同的JNI实现,如果要求这些厂商暴露约定好的一些头文件和库,这不是灵活的设计。
而且使用函数指针表的另外一个好处是: JVM可以根据启动参数动态替换JNI实现。比如:类似于C库,JNI实现为了性能起见,并没有对调用者传入的参数进行检查。但是在调试阶段,也许这种检查是很必要的,帮助你尽早发现BUG。例如如果你使用IBM JDK,你可以指定JVM参数–Xcheck:jni,告诉JVM使用带检查的JNI实现。

参考:
http://java.sun.com/docs/books/jni/html/jniTOC.html
posted @ 2011-02-18 10:59 爱上龙卷风 阅读(7908) | 评论 (1)编辑 收藏

2009年8月7日 #

DLL中导出函数有两种方式,即:dllexport与.def文件。
dllexport方式是:在函数声明中加上__declspec(dllexport);
.def方式是:采用模块定义(.def)文件声明,(.def)文件为链接器提供了有关被链接程序的导出、属性及其他方面的信息。
(关于def文件,可以参考http://msdn.microsoft.com/en-us/library/d91k01sh(VS.80).aspx)
对于这两种方式,需要特别说明的是:
 第一、用.def文件导出的函数,其名称是按我们的意愿定义的,而用__declspec(dellexport)导出时,会有相应的修饰名,具体的话,根据不同的编译器其修饰名也不一样。

 第二、__declspec(dllexport)定义的导出多用于同一编译器的隐式链接(静态调用),而.def导出函数可以确定导出的函数名不会因为不同的编译器而不同,可用于其它开发工具的调用。

有了上面的知识,我们再看JNI环境下的问题。

JNI定义了关键字JNIEXPORT,用于实现DLL中函数的导出的。实际在JNI中,JNIEXPORT被定义为,#define JNIEXPORT __declspec(dllexport),也就是说JNI默认的导出函数使用dllexport方式。我们知道,使用使用dllexport方式产生的导出函数名会根据编译器发生变化,在这种情况下,当Java程序通过Native接口调用DLL本地方法时,可能会发生找不到导出函数的问题。所以,在JNI的情况下,因此最好是定义一个.def文件来指明导出函数,以避免发生UnSatisfiedLinkedException错误 。




posted @ 2009-08-07 22:44 爱上龙卷风 阅读(2543) | 评论 (1)编辑 收藏

2009年3月12日 #

static关键字,有两个作用:
1) 作用于局部变量,定义该变量的存储方式,就是我们常常说的静态局部变量。
2) 作用于用于模块内声明的变量和函数,用于指示其可见性。

先谈谈变量和函数的可见性。在默认的情况下,模块内声明的变量和函数是全局可见的,如下:
//Test1.cpp
struct {  
    
int m;
} test;
//Test2.cpp
struct {  
    
int m;
} test;
BUILD的结果会报"multiply defined symbols found"
如果想避免这样的错误,需要使用static关键字。

不过好像如果你使用typedef,就不会出现这样的错误。
//Test1.cpp
typedef struct {  
    
int m;
} test;
//Test2.cpp
typedef struct {  
    
int m;
} test;
或者:
//Test1.cpp
typedef struct {  
    
int m;
} test;
//Test2.cpp
typedef int test;
关于这一点,我想可能是typedef的作用域是限定在模块内的,所以没有这个问题。这个使用VC和使用GCC都没问题,不过我没有查到官方的文档。

最后一点,想说的是,关于static变量的生命周期。一般来说:static声明的变量初始化,只是在程序运行的第一次被执行。不过有例外,那就是如果该变量定义在dll内,那么该变量的初始化工作是在dll被装载时执行,在这种情况下,程序虽然只运行一次,但是该静态变量可能会被初始化好几次(与dll被装载的次数有关). 这个问题本质上是:static声明的变量的生命周期与包含它的组件相关。从这个意义上,我们可以说,static变量在不同的包含组件(EXE或DLL)上表现出不同行为,移植能力差,容易造成BUG,这样的BUG也不容易发现。我们在开发程序的时候,不可能去假设该静态变量是被包含在什么样的组件里。我想,这也是一个不鼓励使用static变量的一个重要原因。







posted @ 2009-03-12 18:12 爱上龙卷风 阅读(2094) | 评论 (2)编辑 收藏

2009年2月18日 #

       Windows Nativec++应用大量使用了DLL技术。"动态链接"这几字指明了DLLs是如何工作的。对于常规的函数库,链接器从中拷贝它需要的所有库函数,并把确切的函数地址传送给调用这些函数的程序。而对于DLLs,函数储存在一个独立的动态链接库文件中。在创建Windows程序时,链接过程并不把DLLs文件链接到程序上。直到程 序运行并调用一个DLLs中的函数时,该程序才要求这个函数的地址。此时Windows才在DLLs中寻找被调用函数,并把它的地址传送给调用程序。采用这种方法,DLLs达到了复用代码的极限。

      对于DLL, 关键一点是,所有run on windows system 的程序可以共用同一个DLL库,从而达到最大限度的代码复用。并且,由于DLL并不拷贝它需要的所有库函数  这样的话NativeC++程序 executable image size 会比较小。

      modularity的角度,如果要在Java的应用里寻找相对应的DLL的概念,我们会自然地想到jar包。JAR包可以被 Class Loader动态装载进JVM, 不过要几点区别需要说明的是:

第一、从本质上来讲,JAR包是存在于磁盘上的一些data而已(JVM解释执行),而DLLexecutable image

第二、Class Data Sharing (CDS)作为一个新的feature,Java5才被引入,其做法就是:把 system jar 文件打包成为"shared archive",这些"shared archive"会作为memory-mapped in文件存在,共享于不同的JVM 进程间,以减少JVMfootprint,加快Java应用的启动时间。

            值得一提的是:两者都有所谓的HELL问题(JAR HELL vs DLL HELL),新老版本的兼容问题始终让人头疼。

详见解释:

http://en.wikipedia.org/wiki/DLL_hell

http://en.wikipedia.org/wiki/JAR_hell#JAR_hell

    


posted @ 2009-02-18 15:28 爱上龙卷风 阅读(2541) | 评论 (2)编辑 收藏

2009年1月12日 #

题目是这样的:
问:不使用任何循环语句,递归,输出打印n条(n>1) "Hello World"。
解这道题目,利用了c++语言一个非常重要的特性:
c++允许定义基于statck数据区的Object。由此,不由想到了Java.
在Java的世界里,所有的类型都是引用(或者称为指针), 对象内存的分配都是通过new从heap上显式的分配,无法在Java里构建基于statck数据区的对象。所以在Java里,这道题目是无解的。
Java之于c++,既是进步,又是倒退。
Java语言本身的确帮助c++程序员做了很多事情,比如GC, 去掉了c++中很多复杂的特性,比如多重继承,运算符重载等。
同时,c++本身的很多优点,也丧失了。 写了c++, 然后再写java,一个明显的感觉是,没法使用java写出像c++一样简洁的程序。
比如, 没有了运算符重载, 你不得不使用equal方法来表达两个对象的相等。
Java不能显式表达RAII概念,你不得不使用hard code的方法Log方法的进入和退出, 如:
func() {
log("enter func");
//do something.
log("exit func");
}

posted @ 2009-01-12 22:28 爱上龙卷风 阅读(2782) | 评论 (23)编辑 收藏

2009年1月3日 #

我在IBM dwWorks上发了一篇关于Monitor Object 并发设计模式的文章。

Monitor Object 并发模式在 Java 同步机制中的实现。 里面用了很多C++的设计行为,来讨论Java的Monitor Object.
感兴趣的话,欢迎访问
以下是摘要:

文章将从两个方面进行阐述:

  1. 使用 C++ 语言来描述 Monitor Object 设计模式。Java 对于这样一个典型的模式做了很好的语言层面的封装,因此对于 Java 的开发者来说,很多关于该模式本身的东西被屏蔽掉了。本文试图使用 Native C++ 语言,帮助读者从本质上对 Monitor object 设计模式有一个更全面的认识。
  2. 结合 C++ 版本的 Monitor Object 设计模式,引领读者对于 Java 同步机制有一个更深刻的认识,帮助读者正确有效地使用 Java 同步机制。


posted @ 2009-01-03 18:26 爱上龙卷风 阅读(2450) | 评论 (2)编辑 收藏

2008年2月3日 #

        一般来讲, 在服务器上,如果有足够的资源,Winsock server,理论上可以支持成千的并发连接。而现实是,我们没有足够的资源可供使用,分配。本文主要来讨论一下内存资源之于Winsock server开发的重要性。
一)基本概念。
-> Pages,Locked Pages.
        在现代操作系统中,内存管理会把主存(RAM)分成Pages来管理。 Paging(或者swapping)指的是主存与外存之间以Page为单位进行数据的交换。Locked Pages指的是被锁定在主存中的内存页,以保证一些内核组件,driver可以访问到它们。windows一定会保证一定数量的可交换的内存空间,防止一些非法程序锁定所有的物理内存,而致使系统崩溃。在windows NT, windows 2000上,可锁定的内存总的大小上限大概是物理内存的1/8(当然对于程序的开发人员,不应该对这个值进行任何的假设,这个值可能会随着操作系统本版的变化而变化)。在Winsock应用开发过程中,以overlapped方式读写IO操作,将会导致内存被锁定。
-> working set
        在程序开始运行,并达到其稳定的运行状态(主要指的是其对内存的使用),在这个状态下,程序使用内存的数量一般小于其需要使用内存的总量。这样一个稳定的运行状态,我们可以称为working set: 被该程序频繁访问的内存页的集合。在windows上,你可以使用SetWorkingSetSize Win32 API来增加程序使用物理内存的数量。
-> non-paged pool
       不可交换的内存。这主要指以non-paged的方式分配的内存,这些内存就像locked pages一样,是从来不会被交换出去的,用来存放一些由内核组件,driver访问的信息。 在Winsock应用开发过程中,以下的操作可能导致分配non-paged内存。
1) 调用系统一些系统的API,例如打开文件,create socket,等,都会导致从non-paged pool分配内存。
2) 一些driver可以显式地从该区域分配内存。
二) Winsock server上Locked Pages使用。
        我们提到过,任何的overlapped IO操作,都会导致锁定内存页。这些内存页一旦被locked,就不会被交换出去。我们知道,windows操作系统对最大的可锁定内存页做了一个上限,如果超出这个上限,overlapped IO调用将会导致WSAENOBUFS错误。
        考虑下面的情况,如果server在每个连接上会发出很多的overlapped receives操作,那么,随着连接数目的增多,很明显,被锁定的内存数量很有可能达到上限而导致WSAENOBUFS错误。在这种情况下,如果服务器预期会处理大数量的客户端连接,则需要服务器在每个连接上发出zero-byte buffer的overlapped接收请求(这种情况下,因为the size of buffer is zero,所以没有任何内存被锁定),一旦overlapped接收操作完成,server可以以non-blocking方式执行receive操作,以取得所有缓存在so_rcvbuf中的数据,直到返回WSAEWOULDBLOCK为止。
        另外需要注意的是,windows在page的边界上对内存进行锁定,在x86平台上,它是4kb的整数倍。所以,假如你post了一个1 KB buffer,而系统真实锁定的是4 KB 的大小,为了避免这样的浪费,尽量用4kb的整数做overlapped  IO操作。
三) Winsock server上non-paged pool使用。
        同Locked Pages限制一样,windows对non-paged pool也有一个最大的限制。并且,当你的应用出现这个问题的时候,超出它的最大限制数,情况要远比Locked Pages复杂。这种情况下,后果是不确定的,有可能你的Winsock调用返回WSAENOBUFS错误,也有可能,在系统中,一个和你的应用毫无关联的driver由于申请不到non-paged内存而致使system crash。而这样的灾难,是没法恢复的。
        考虑一个具体的例子:我们假设在windows2000上,系统有1 GB内存。这样的配置下,windows大概会预留1/4的空间用作non-paged pool(同样,对于程序的开发人员,不应该对这个值进行任何的假设),即:256MB。这样的配置下,保守估计,我们的Winsock server能够处理到大概50,000连接,或者更多。(每个accepted socket大概消耗1.5kb,每个连接上post一个overlapped操作,分配一个IRP,大概需要500 byte, 总计:(1500+500)*50,000 = 100 Mb) 。
       无论是对于Locked Pages,还是对于non-paged pool使用,一旦超出了上限,Winsock调用仅仅会返回一般的WSAENOBUFS 或者ERROR_INSUFFICIENT_RESOURCES错误。为了处理这些错误,你可以试试以下的方法:
1) 需要首先调用SetWorkingSetSize,增加应用的可支配资源数,看能否解决。
2)     确信你的应用没有做出太多的overlapped  IO操作。
3) 关闭一些连接数。
四) SOCKET的缓冲区设置问题。

         Winsock在默认的情况下,每个socket都会与一个send和receive buffer相关联。你可以通过调用setsockopt来设置buffer的大小。
        在缓冲区没有被关闭的情况下,我们看看overlapped send和revc是怎么工作的。
        当上层的应用做出了send调用,而这时如果send buffer还有剩余的空间,那么数据将会从用户提交的buffer复制到send buffer中,然后调用返回成功。否则,假如这时send buffer已满,用户提交的buffer将会被锁定,并且调用返回WSA_IO_PENDING。当send buffer的数据被下层的tcp处理完成,winsock将直接处理用户提交的buffer里的数据,而不需要再复制。
        同样,对于recv操作,如果数据已经被缓存在socket的receive buffer里,当发生recv调用的时候,数据将直接从socket的receive buffer复制到用户的buffer里,recv调用返回成功。否则,假如发生调用时receive buffer里没有数据,用户提交的buffer将会被锁定,recv调用返回WSA_IO_PENDING。当数据到达当前连接,将会被直接复制到用户提交的buffer里。
        一个应用程序通过设定send buffer为0,把缓冲区关闭,然后发出一个阻塞send()调用。在这样的情况下,系统内核会把应用程序的缓冲区锁定,直到接收方确认收到了整个缓冲区后send()调用才返回。似乎这是一种判定你的数据是否已经为对方全部收到的简洁的方法,实际上却并非如此。想想看,即使远端tcp通知数据已经收到,其实也根本不代表数据已经成功送给客户端应用程序,比如对方可能发生资源不足的情况,导致afd.sys不能把数据拷贝给应用程序。另一个更要紧的问题是,在每个线程中每次只能进行一次发送调用,效率极其低下。
        另外,希望通过关闭Winsock缓冲区,从而避免数据复制,达到优化性能的目的,也是不可取的。从上面,我们看到:只要应用保证适量的,足够的send, recv调用,这样的复制是完全可以避免的。
        高性能的服务器应用程序可以关闭发送缓冲区,同时不会损失性能。不过,这样的应用程序必须十分小心,保证它总是发出多个重叠发送调用,而不是等待某个重叠发送结束了才发出下一个。如果应用程序是按一个发完再发下一个的顺序来操作,那浪费掉两次发送中间的空档时间,总之是要保证传输驱动程序在发送完一个缓冲区后,立刻可以转向另一个缓冲区。
        如果关闭了recv buffer,在你的应用没有保证足够的recv操作前提下,任何进来数据,必须在TCP层进行缓存,最大缓存的数量将取决于tcp windows的大小(17Kb)。而最为严重的是这些缓存是从non-paged pool分配而来。如上所述,non-paged pool是非常珍贵,稀缺的内存。所以,从这个意义上来讲,关闭了recv buffer操作是不可取的。
    这是2007年最后一篇帖子,最后祝大家新年快乐,过个好年!!!

posted @ 2008-02-03 15:18 爱上龙卷风 阅读(3040) | 评论 (2)编辑 收藏

2008年1月5日 #

四、c++中的多态规则。
一) c++中函数动态绑定规则。
看下面的例子:

class Window
{

public:
  virtual 
void  oops()
  
{
    cout
<<"Window oops"<<endl;
  }

public:
  
int height;
  
int width;
}
;
class TextWindow : public Window
{

public:
  virtual 
void  oops()
  
{
    cout
<<"TextWindow oops"<<cursorLocation<<endl;
  }

public:
  
int cursorLocation;
}
;

main()
{
  Window win;
  Window
* tWin;
  TextWindow 
* tWinPtr;

  tWinPtr 
= new TextWindow;
  tWin 
= tWinPtr;
  
  win.oops();
  tWin
->oops();
}


类TextWindow继承与类Window。我想程序运行的结果,大多数熟悉C++的人都会知道,
win.oops()最终调用的是父类oops函数,而tWin->oops()调用的是子类TextWindow的函数。
通过这个例子,我们先总结一下c++中的多态调用规则
第一、对于指针和引用类型,当消息调用的成员函数有可能被重写时,最终被选择调用的成员函数由消息接收者的动态类型确定(注意:在OO概念中,对某个对象成员函数进行调用,常常称为给该对象发送消息,该对象就是消息的接收者)。
      如上例:tWin->oops(),由于tWin的动态类型是子类TextWindow,而不是Windows,所以tWin->oops()调用的是子类TextWindow的函数。
二、对于其它的变量,对虚拟函数调用绑定完全由该变量的静态类型确定(即该变量的声明),而不是该变量的真实类型确定。
     如上例:win.oops(),由于win的声明类型为Window类,所以其结果调用的是父类oops函数。
二) 探讨。
     接下来,我们要看的问题是,在c++中,为什么对于多态规则(或者说是动态函数绑定规则),做出了两中不同的划分,即:只有指针与引用类型,才进行函数的后期动态绑定,也就是多态。这或许也是许多c++初学者非常迷惑的地方。这种规则的不一致性,的确给c++的语法造成一定的复杂性。而这在Java,或者C#中是没有的,后面我们会涉及到。
      我们先来看例子。 

void f()
{
  Window  win;
  Window
* tWinPtr;
  
  tWinPtr 
= new TextWindow;
  win     
= *tWinPtr;//what's problem happen
   
  win.oops(); 
//what's problem happen
  tWinPtr->oops();
}


      在这里,如果我们假设,c++的函数动态绑定规则是一致的,看看会发生什么问题???
      现在win被声明为Window类型,然而其真实的类型为TextWindow(因为win=*tWinPtr),由于我们的假设,win现在是允许进行动态函数绑定的,所以当执行win.oops()时,实际上是调用子类TextWindow的成员函数。
     现在,我们有必要来审视一下win变量的内存布局。由于win变量是在栈上声明的变量,其内存也是从栈进行分配(这是c++从c语言那里继承过来的优良特质,从栈上分配内存空间比动态分配内存有更好的执行速度),c++标准规定:给win变量分配内存空间的大小,由其静态的类型确定,即应该是Window类所使用的内存空间大小。在这种情况下,当执行win=*tWinPtr时,什么会发生?如下图:

在默认的拷贝构造函数情况下,信息会出现丢失,这就是著名的slicing off现象。结果,变量cursorLocation在win的内存空间里丢失了。然而,问题是:在我们假设下,我们要求win.oops()导致TextWindow的成员函数调用,而在这个函数中,访问到的cursorLocation变量是不存在!win.oops()调用将导致内存违例!
      到这里,我们可以总结一下:c++标准基于的其特定的内存分配规则,给出了以上,我们在前一节总结出的函数动态绑定规则。
三) 深入。
      当然,我们也可以说,c++也可以通过改变其内存分配规则,来给出一个一致性的函数动态绑定规则。比如:可以考虑在给win变量分配内存空间时,考虑其所有子类需求,然后分配最大数量的内存空间给win变量。这种做法可行性很差,对于编译器而言,需要扫描整个程序(确定该类的所有子类),才能确定最大的内存空间是多少。在使用类库或者框架的情况下,会引起整个类库,框架的重新编译,这是得不偿失的!而这种做法,在oo的语言中,基本上是没有的。这也是c++不得不基于其现有的内存管理机制,而对多态规则作出的不一致的解释。
    对于这个c++现有的内存管理机制,我们如果从另外角度去理解的话,是很合理的。当win=*tWinPtr发生
时,我们可以类似地认为:好比一个float类型的数赋给了一个interger类型的变量,其结果当然是float的值被截断了。
    我们再来看其它语言,Java(或者C#)是怎么解决的。
    最重要的一点是,在Java(C#)中只有引用的概念,所以在栈上声明的类的变量,只需要分配一个指针大小的内存空间就行了,而不需要给该变量分配空间来保存变量内容本身,这其实就是我们现在看到的c++中指针和引用的情况。
    

 

posted @ 2008-01-05 23:12 爱上龙卷风 阅读(3127) | 评论 (8)编辑 收藏

2007年11月11日 #

        C++之父Bjarne stroustrup曾经说过:不需要了解所有的c++细节,也能够写出好的c++程序;不应该注重语言方面的特征,而应该注重软件设计技术本身。很显然,我的这篇文章,与这两句话背道而驰:).的确,我们程序员,不应该把精力放在c++本身语言的特征上,而是应该思考软件设计技术本身。那么,在我们需要提高对c++理解的同时,是不是我们从下面几个方面为着眼点
1) 从编译原理的角度
2) 从技术需求的角度
3) 从软件设计技术的角度
从以上的几个角度,来重新审视c++一些晦涩语法,或许,我们能从中获益。在这里,我要说的是,我们不单单是要记住这些c++语言特性怎么样的使用,而是应该知道这些语言特性背后隐藏的故事,以便于我们更深层次地理解c++,理解软件设计。
一、子类通过函数名字隐藏父类函数。
如下例:

class Base
{
public:
 virtual 
void f(int x);
}
;
class Derived: public Base
{
public:
 virtual 
void f(double* pd);
}
;
int main()
{
  Derived
* pd = new Derived();
  pd
->f(10); //compile error!!!
}

         当我们编译pd->f(10)操作时,编译器报错。按照我们常规的理解是:父类的函数void f(int x)与子类的函数void f(double*pd),由于参数类型不同,其函数签名也是不一样的,按照这样的逻辑,在这个类继承体系中,这两个函数完全应该是互不隐藏的,我们完全可以认为是符合overloaded规则的两个函数。
        但是,在c++里,子类通过函数名字隐藏父类函数,而不是通过函数签名!c++给出的解释也是合理的:试想一种情况:你使用了别人写的类库,继承其中的某个类,写了你自己的子类。
如上面的例子,你的子类就是Derived,而类库中的父类就是Base.当你根本不知道在父类中还有这样一个f(int x)函数时,在调用子类Derived的f函数时,你犯了错误,参数类型传成了int类型(或者不是你犯的错误,编译器帮你自动转化为int类型),结果是:程序可以正常运行,但是,执行的结果却不是你所期望的,是f(int x)调用,而不是你自己的实现:f(double* pd)调用!
         这就是c++为什么通过函数名字隐藏父类函数的原因。
        说到这里,我们需要补充几句:虽然c++在语言层面上给我们提供了这样的保证,但是,子类hide父类的函数,这是一个非常不好的设计。从OO的角度出发,应该讲求的是Liskov Substitution Principle。即:suntypes must be substitutable fro their base types.很显然,当hide行为发生时,从接口的角度来讲,子类与父类是不能互为替代的。父类的protected or public的方法,应该很自然地由其所有子类所继承,而不是被隐藏。隐藏行为的发生,相当于在这套继承体系中开的一个后门。很显然,C++帮助我们自动隐藏了父类的方法,但是,作为程序开发的我们,应该意识到这一点,也应该避免这样的设计。
二、c++的per-class allocator语法规则
          在D&E of C++一书中,Stroustrup给出了几点c++提供per-class allocator的理由,这些理由也是我们使用class level的allocator的原因,所以,有必要我们总结一下:
第一、许多程序应用,需要在运行的过程中,大量地Create和Delete对象。这些对象,诸如:tree nodes,linked list nodes,messages等等。如果在传统的heap完成这些对象的创建,销毁,由于大量的内存申请,释放,势必会造成内存碎片。这种情况下,我们需要对内存分配进行细粒度的控制。
第二、一些应用需要长时间跑在内存受限的装置上,这也需要我们对内存分配进行细粒度的控制,而不是无限制地分配,释放。
主要基于以上的两点,c++提供了per-class allocator语言支持。
如下例:

class X
{
public:
  
void* operator new(size_t sz); //allocate sz bytes
  void  operator delete(void* p) //free p;
}
;

      new操作符函数负责对象X的内存分配。对这样一个语法规则,我们好奇的是,为什么声明了一个我们从来都不使用的参数size_t sz.我们的使用语法如下: X* px = new X;
C++也给出了解释:per-class allocator机制将适用整个类的继承体系。例如:

class Y: public X //ojects of class Y are also allocated using X::operator new
{
  
//
  
// 
}
;

        对于子类Y,其内存分配函数也是X::operator new()。但是,在这里,内存分配的大小,不应该是sizeof(X),而是sizeof(Y).问题的关键在这里:C++通过提供多余的参数size_t sz,而给开发者提供了更大的灵活性,也即:per-class allocator是面向类的继承体系的内存管理机制,而不单单是面向单个类。
三、Koenig Lookup机制。
        大家对Andrew Koenig应该很熟悉,c++大牛,是AT&T公司Shannon实验室大规模编程研究部门中的成员,同时他也是C++标准委员会的项目编辑。他拥有超过30年的编程经验,其中有15年的C++使用经验。
        Koenig Lookup,就是以Andrew Koenig命名的查找规则。在看这个定义之前,我们先弄清楚函数所在的域的分类,一般来讲,分为:
1:类域(函数作为某个类的成员函数(静态或非静态))
2:名字空间域
3:全局域(即C++默认的namespace)
        而Koenig Lookup机制,就是当编译器对无限定域的函数调用进行名字查找时,除了当前名字空间域以外,也会把函数参数类型所处的名字空间加入查找的范围。
如下例:

#include <iostream>
using namespace std;
namespace Koenig
{
    
class MyArg
    
{
    
public:
         ostream
& print(ostream& out) const
         
{
            out
<<"this is MyArg."<<endl;
         }

    }
;
 
    inline ostream
& operator<<(ostream& out, const MyArg& myArg)
    
{
         
return myArg.print(out);
    }

}

 
int main()
{
    Koenig::MyArg myArg;
    cout
<<myArg;
    
return 0;
}

       如上的代码,使用operator<<操作符函数,打印对象的状态,但是函数ostream& operator<<(ostream& out, const MyArg& myArg) 的定义域是处于名字空间Koenig中,为什么编译器在解析main函数(全局域)里面的operator<<调用时,它能够正确定位到Koenig名字空间里面的operator<<?这是因为根据Koenig查找规则,编译器需要把参数类型MyArg所在的名字空间Koenig也加入对ostream& operator<<(ostream& out, const MyArg& myArg) 调用的名字查找范围中。
       
如果没有Koenig查找规则,我们就无法直接写cout<<myArg;,而是需要写类似Koenig::operator<<(std::cout, myArg); 这样的代码(使用完全限定名)。这样的结果是,即不直观也不方便。

        其实在C++里,提供了很多类似于Koenig查找规则的机制,以保证程序语法上的简洁,明了。例如:许多的操作符函数,COPY构造函数。而这些,也是我们写出专业的C++程序的基本。
未完待续:)

 

 


 

posted @ 2007-11-11 14:56 爱上龙卷风 阅读(4569) | 评论 (26)编辑 收藏

2007年8月26日 #

         本文主要探讨一下windows平台上的完成端口开发及其与之相关的几个重要的技术概念,这些概念都是与基于IOCP的开发密切相关的,对开发人员来讲,又不得不给予足够重视的几个概念:
1) 基于IOCP实现的服务吞吐量
2)IOCP模式下的线程切换
3)基于IOCP实现的消息的乱序问题。

一、IOCP简介
    提到IOCP,大家都非常熟悉,其基本的编程模式,我就不在这里展开了。在这里我主要是把IOCP中所提及的概念做一个基本性的总结。IOCP的基本架构图如下:
 

如图所示:在IOCP中,主要有以下的参与者:
--》完成端口:是一个FIFO队列,操作系统的IO子系统在IO操作完成后,会把相应的IO packet放入该队列。
--》等待者线程队列:通过调用GetQueuedCompletionStatus API,在完成端口上等待取下一个IO packet。
--》执行者线程组:已经从完成端口上获得IO packet,在占用CPU进行处理。
除了以上三种类型的参与者。我们还应该注意两个关联关系,即:
--》IO Handle与完成端口相关联:任何期望使用IOCP的方式来处理IO请求的,必须将相应的IO Handle与该完成端口相关联。需要指出的时,这里的IO Handle,可以是File的Handle,或者是Socket的Handle。
--》线程与完成端口相关联:任何调用GetQueuedCompletionStatus API的线程,都将与该完成端口相关联。在任何给定的时候,该线程只能与一个完成端口相关联,与最后一次调用的GetQueuedCompletionStatus为准。
二、高并发的服务器(基于socket)实现方法
        一般来讲,实现基于socket的服务器,有三种实现的方式(thread per request的方式,我就不提了:)):
第一、线程池的方式。使用线程池来对客户端请求进行服务。使用这种方式时,当客户端对服务器的连接是短连接(所谓的短连接,即:客户端对服务器不是长时间连接)时,是可以考虑的。但是,如若客户端对服务器的连接是长连接时,我们需要限制服务器端的最大连接数目为线程池线程的最大数目,而这应用的设计本身来讲,是不好的设计方式,scalability会存在问题。
第二、基于Select的服务器实现。其本质是,使用Select(操作系统提供的API)来监视连接是否可读,可写,或者是否出错。相比于前一种方式,Select允许应用使用一个线程(或者是有限几个线程)来监视连接的可读写性。当有连接可读可写时,应用可以以non-bolock的方式读写socket上的数据。使用Select的方式的缺点是,当Select所监视的连接数目在千的数量级时,性能会打折扣。这是因为操作系统内核需要在内部对这些Socket进行轮询,以检查其可读写性。另一个问题是:应用必须在处理完所有的可读写socket的IO请求之后,才能再次调用Select,进行下一轮的检查,否则会有潜在的问题。这样,造成的结果是,对一些请求的处理会出现饥饿的现象。
        一般common的做法是Select结合Leader-Follower设计模式使用。不过不管怎样,Select的本质造成了其在Scalability的问题是不如IOCP,这也是很多high-scalabe的服务器采用IOCP的原因。
第三、IOCP实现高并发的服务器。IOCP是实现high-scalabe的服务器的首选。其特点我们专门在下一小姐陈述。
三、IOCP开发的几个概念
第一、服务器的吞吐量问题。
      我们都知道,基于IOCP的开发是异步IO的,也正是这一技术的本质,决定了IOCP所实现的服务器的高吞吐量。
       我们举一个及其简化的例子,来说明这一问题。在网络服务器的开发过程中,影响其性能吞吐量的,有很多因素,在这里,我们只是把关注点放在两个方面,即:网络IO速度与Disk IO速度。我们假设:在一个千兆的网络环境下,我们的网络传输速度的极限是大概125M/s,而Disk IO的速度是10M/s。在这样的前提下,慢速的Disk 设备会成为我们整个应用的瓶颈。我们假设线程A负责从网络上读取数据,然后将这些数据写入Disk。如果对Disk的写入是同步的,那么线程A在等待写完Disk的过程是不能再从网络上接受数据的,在写入Disk的时间内,我们可以认为这时候Server的吞吐量为0(没有接受新的客户端请求)。对于这样的同步读写Disk,一些的解决方案是通过增加线程数来增加服务器处理的吞吐量,即:当线程A从网络上接受数据后,驱动另外单独的线程来完成读写Disk任务。这样的方案缺点是:需要线程间的合作,需要线程间的切换(这是另一个我们要讨论的问题)。而IOCP的异步IO本质,就是通过操作系统内核的支持,允许线程A以非阻塞的方式向IO子系统投递IO请求,而后马上从网络上读取下一个客户端请求。这样,结果是:在不增加线程数的情况下,IOCP大大增加了服务器的吞吐量。说到这里,听起来感觉很像是DMA。的确,许多软件的实现技术,在本质上,与硬件的实现技术是相通的。另外一个典型的例子是硬件的流水线技术,同样,在软件领域,也有很著名的应用。好像话题扯远了,呵呵:)
第二、线程间的切换问题。
         服务器的实现,通过引入IOCP,会大大减少Thread切换带来的额外开销。我们都知道,对于服务器性能的一个重要的评估指标就是:System\Context Switches,即单位时间内线程的切换次数。如果在每秒内,线程的切换次数在千的数量级上,这就意味着你的服务器性能值得商榷。Context Switches/s应该越小越好。说到这里,我们来重新审视一下IOCP。
     完成端口的线程并发量可以在创建该完成端口时指定(即NumberOfConcurrentThreads参数)。该并发量限制了与该完成端口相关联的可运行线程的数目(就是前面我在IOCP简介中提到的执行者线程组的最大数目)。当与该完成端口相关联的可运行线程的总数目达到了该并发量,系统就会阻塞任何与该完成端口相关联的后续线程的执行,直到与该完成端口相关联的可运行线程数目下降到小于该并发量为止。最有效的假想是发生在有完成包在队列中等待,而没有等待被满足,因为此时完成端口达到了其并发量的极限。此时,一个正在运行中的线程调用GetQueuedCompletionStatus时,它就会立刻从队列中取走该完成包。这样就不存在着环境的切换,因为该处于运行中的线程就会连续不断地从队列中取走完成包,而其他的线程就不能运行了。
     完成端口的线程并发量的建议值就是你系统CPU的数目。在这里,要区分清楚的是,完成端口的线程并发量与你为完成端口创建的工作者线程数是没有任何关系的,工作者线程数的数目,完全取决于你的整个应用的设计(当然这个不宜过大,否则失去了IOCP的本意:))。
第三、IOCP开发过程中的消息乱序问题。
     使用IOCP开发的问题在于它的复杂。我们都知道,在使用TCP时,TCP协议本身保证了消息传递的次序性,这大大降低了上层应用的复杂性。但是当使用IOCP时,问题就不再那么简单。如下例:
  
       三个线程同时从IOCP中读取Msg1, Msg2,与Msg3。由于TCP本身消息传递的有序性,所以,在IOCP队列内,Msg1-Msg2-Msg3保证了有序性。三个线程分别从IOCP中取出Msg1,Msg2与Msg3,然后三个线程都会将各自取到的消息投递到逻辑层处理。在逻辑处理层的实现,我们不应该假定Msg1-Msg2-Msg3顺序,原因其实很简单,在Time 1~Time 2的时间段内,三个线程被操作系统调度的先后次序是不确定的,所以在到达逻辑处理层,
Msg1,Msg2与Msg3的次序也就是不确定的。所以,逻辑处理层的实现,必须考虑消息乱序的情况,必须考虑多线程环境下的程序实现。
        在这里,我把消息乱序的问题单列了出来。其实在IOCP的开发过程中,相比于同步的方式,应该还有其它更多的难题需要解决,这也是与Select方式相比,IOCP的缺点,实现复杂度高。
结束语:
    ACE的Proactor Framework, 对windows平台的IOCP做了基于Proactor设计模式的,面向对象的封装,这在一定程度上简化了应用开发的难度,是一个很好的异步IO的开发框架,推荐学习使用。 
Reference:
    Microsoft Technet,Inside I/O Completion Ports

posted @ 2007-08-26 16:06 爱上龙卷风 阅读(16362) | 评论 (23)编辑 收藏

仅列出标题  下一页