sherrylso

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

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 爱上龙卷风 阅读(889) | 评论 (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 爱上龙卷风 阅读(1434) | 评论 (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 爱上龙卷风 阅读(1841) | 评论 (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 爱上龙卷风 阅读(2349) | 评论 (23)编辑 收藏

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 爱上龙卷风 阅读(3570) | 评论 (18)编辑 收藏

2007年8月12日 #

摘要:
        在对多线程并发的编程环境下,死锁是我们经常碰到的和经常需要解决的问题。所谓死锁,即:由于资源占用是互斥的,当某个线(进)程提出申请资源后,使得有关进程在无外力协助下,永远分配不到必需的资源而无法继续运行,这就产生了一种特殊现象死锁,如下图:

        线程#1在获得Lock A后,需要获得Lock B,而同时,线程#2在Lock B后,需要获得Lock A。对于线程#1和#2,由于都不能获得满足的条件,而无法继续执行,死锁就形成了。
        死锁是多线程并发编程的大难题,我们可以通过Log Trace、多线程编程辅助工具、IDE调试环境等手段进行调试、跟踪。然而,另一个更难对付的问题是“假死锁”(我在这里暂且称为“假死锁”,实在找不到什么更好的称呼)。所谓的假死锁,我给出的定义是:在有限的时间内的死锁。与死锁不同的是,其持续的时间是有限的,而大家都知道,死锁持续的时间是无限的,如果碰到死锁,程序接下来是什么都干不了了。而正是由于假死锁的相对的持续时间,给我们编程人员会带来更大的麻烦。可以想象得到,我们想通过某些工具来Trace这样一个特定的时间段是非常困难的,更多的情况下,我们需要结合LOG进行合理的分析,使得问题得以解决。本文就假死锁产生的条件,环境,以及解决的办法做一个讨论。
一、假死锁的产生条件。

    考虑下面的例子(我只是给给出了伪代码),假设我们系统中的线程个数是确定的,有限的。在本例中,系统总的线程数目是3。如下图:

线程#1,#2,#3都可能被调度进入临界区A,我们假设线程#1执行临界区A时花费了10s的时间,而在这10s的时间里,线程#2与线程#3都处于等待的状态。也就是说:在这个10s的时间里,系统是没法响应任何的其他请求。我们称之为10s的假死锁。如果在这段时间里,系统需要一些关键的请求被执行,这些关键请求是需要real time地被处理,比如说是Timer事件,则后果是不堪设想的。(注意:我们的假定是系统中的线程只有#1,#2,#3)。
       以此,总结一下发生假死锁的条件,如下:
--〉临界区的代码在集中的时间段内,可能被系统中的任意线程执行,完全由操作系统决定。
--〉临界区的代码在某些情况下,可能是很耗时的。(比如:其执行时间大于100ms,或者,甚至是秒级别的)
二、在Proactor(IOCP)中的假死锁。
        在前面的文章中,我提到过在windows平台上,Proactor设计模式是基于IOCP的。在这里,本文不会用过多的语言来阐述Proactor是怎样的设计,重点放在Proactor的假死锁及其一些解决的办法。另外需要说明的是,我这里所说的Proactor,在技术层面上,等同于IOCP,我们也可以按照IOCP来理解我所阐释的概念。
        我们都知道,IOCP是靠工作者线程来驱动的。工作者线程与一个完成端口对象相关联,当IO 请求被投递到完成端口对象时,这些线程为完成端口服务。需要说明的是,应该创建多少个线程来为完成端口服务,是你的应用设计来决定的(很重要的的一点是:在调用CreateIoCompletionPort时指定的并发线程的个数,和创建的工作者线程的个数是有区别的,详细的技术细节,请参考其他资料)。但是总的来说,在你的系统交付运行后,工作者线程的线程数目是一个确定的值。其结构图,大致如下:

         我们假定使用了线程数目为4的工作者线程来为完成端口服务,它们通过调用来GetQueuedCompletionStatus方法来从完成端口中获取IO相关的packet,一旦获得,它们都会回调业务逻辑层的代码来进行相关的业务逻辑处理。到这里我们看到,假设,在业务逻辑层存在临界互斥区,并且在某一个集中的时间段内,工作者线程都可能被调度执行该临界互斥区,那么,假死锁的条件基本形成,如果某一个线程在该区域花费的时间比较长,假死锁就会发生。
        一般来说,解决这样的问题的关键就是打破形成假死锁的条件:
       第一、在回调函数里,尽量减少锁的使用。
       第二、减量减少临界互斥区的执行时间。对于一些慢速的操作尤其注意。比如:当你在临界互斥区访问慢速的IO操作时(打开文件,读写文件等),可能需要考虑Cache机制,通过使用内存来代替慢速的disk。
       第三、将临界互斥区代码委托给另外独立的线程(或线程组)执行,代价是增加这些线程间的通讯。
       第四、通过使用流控等手段,避免让所有的线程在集中的时间段内访问该临界互斥区。
三、结束语:

         事实上,类似这样的问题,一旦存在,是很难发现和调试的。不过对于多线程的编程,我们都应该遵守以下的基本原则,以最大化的防止死锁和假死锁的发生。

         --> 尽量减少锁的使用频率和保护范围。
         --> 当线程在互斥锁的保护范围内执行代码时,应该:尽量减少对慢速IO设备的访问(如:disk),尽量避免获得其它互斥资源。
         --〉正确使用各种锁,包括:原子操作原语,Read Lock, Write Lock, 和Recursive Lock等。这些锁在不同的场景下有着不同的作用。
posted @ 2007-08-12 15:41 爱上龙卷风 阅读(1393) | 评论 (9)编辑 收藏

2007年7月28日 #

     一般来说,基于CS(client-server)软件架构的开发技术有很多种。比较常用的有:基于socket的网络编程、RPC、基于Java技术的RMI(当然C#也有类似技术)、CORBA等。在这里我们只是对基于socket的网络编程与RMI作个对比,有助于我们了解它们各自的应用领域,帮助我们在面对一个具体问题的时候选用适合的技术。另外,本文所做的讨论可以认为是脱离了语言层面的东西,只是对技术的本身做一个讨论,无关乎你是用C++、C#或Java 在开发。
一、RMI技术简介
        本文就以Java为例,简单介绍一下RMI技术。
        从Java1.1开始,远程方法调用作为Java分布式对象技术成为Java核心的API之一(在java.rmi.* 包)。RMI的引入,使得Java程序之间能够实现灵活的,可扩展的分布式通信。RMI允许Java对象存在于多个不同的地址空间,分布在不同的Java虚拟机上。每一个地址空间可以在同一台主机上或者网络上不同的计算机上。由于远程方法调用跨越不同的虚拟机边界到不同的指定的地址空间,所以没有对象共享的全局变量,这就需要对象序列化(Object Serialization)API,它使得Java对象能够在不同的JVM之间传递。对象序列化是特别为Java的对象设计的,这就意味着Java程序中的对象可以作为对象参数存取(可序列化的对象必须实现Serializable接口)。结合RMI和对象序列化机制,就可以访问越过本地Java虚拟机边界的对象以及数据。通过RMI,可以调用远程对象的远程方法,而通过Java对象序列化机制可以将对象传递给这些方法。
        最基本的Java模型并没有提供将远程主机上的Java对象看作本地Java程序地址空间一部分的能力,而RMI祢补了这一不足。另外,由于Java与硬件平台无关的特性,无论是同构的系统还是异构的系统,RMI不需移植就可以顺利运行。
       RMI为Java平台的分布式计算提供了一个简单而直接的模型。因为Java的RMI技术是基于Java平台的,所以它将Java平台的安全性和可移植性等优点带到了分布式计算中。RMI大大扩展Java的网络计算能力,它为编写基于分布式对象技术的企业级Internet/Intranet应用提供了强大的系统平台支持。
      Java RMI体系结构如下图:


二、基于socket的网络编程
        当你使用socket进行网络应用开发的时候,一般的思路是“消息驱动逻辑”,即这样的软件系统一般具有以下特点:
       (1) 客户端与服务器端依靠消息进行通讯。
       (2) 客户端或者服务器端都需要一个消息派遣器,将消息投递给具体的massage handler
       (3) 客户端或者服务器端利用massage handler进行逻辑事务处理
 见下图:

        使用socket开发的软件系统,从技术的本质上来讲,有以下几个特点:
        (1) 基于TCP协议的通讯
        (2) 应用程序本身需要提供对消息的序列化处理(所谓的序列化指的是将消息输出到网络流中)
        (3) 客户端与服务器端需要事先商议好它们之间的通讯协议即它们交互的消息格式
        (4) 由于是消息驱动逻辑,从本质上决定了这样的编程模式很难面向对象化
三、RMI Vs Sochet
        RMI技术比较socket的网络编程主要有以下几个方面:
        第一、.RMI是面向对象的,而后者不是。
        第二、.RMI是与语言相绑定的。比如当你使用Java RMI技术的时候,客户端与服务器端都必须使用Java开发。而socket的网络编程是使用独立于开发语言的,甚至独立于平台。基于socket的网络编程,客户端与服务器端可以使用不同开发语言和不同的平台。
       第三、从网络协议栈的观点来看,RMI与socket的网络编程处于不同层次上。基于socket的网络编程位于TCP协议之上,而RMI在TCP协议之上,又定义了自己的应用协议,其传输层采用的是Java远程方法协议(JRMP)。可见,在网络协议栈上,基于RMI的应用位置更高一些,这也决定了,与socket的网络编程相比,RMI会丧失一些灵活性和可控性,但是好处是它带给了应用开发者更多的简洁,方便和易用。比如:如果你用的是RMI,你不需要关心消息是怎么序列化的,你只需要像本地方法调用一样,使用RMI。代价是:应用开发者无法很好地控制消息的序列化机制。
      第四、这是最后一点不同,我认为也是比较重要的一点,就是两种方法的性能比较,其往往决定着你将使用那种技术来开发你的应用。以下引用Adrian Reber在Network-programming with RMI文中对TCP和RMI所做的一个比较,其做的实验主要是对两者在网络传输的带宽上作的对比: 在网络上传输2 byte的有效数据,对于TCP而言,总共有478 byte被额外传输,而对于RMI, 1645byte被额外传输。
以下是两者的trace结果:
TCP:
46037 > 12345 [SYN] Seq=801611567 Ack=0 Win=5840 Len=0
12345 > 46037 [SYN, ACK] Seq=266515894 Ack=801611568 Win=10136 Len=0
46037 > 12345 [ACK] Seq=801611568 Ack=266515895 Win=5840 Len=0
12345 > 46037 [PSH, ACK] Seq=266515895 Ack=801611568 Win=10136 Len=1
46037 > 12345 [ACK] Seq=801611568 Ack=266515896 Win=5840 Len=0
12345 > 46037 [FIN, PSH, ACK] Seq=266515896 Ack=801611568 Win=10136 Len=1
46037 > 12345 [RST, ACK] Seq=801611568 Ack=266515898 Win=5840 Len=0
RMI:
42749 > rmiregistry [SYN, ECN, CWR]
Seq=3740552479 Ack=0 Win=32767 Len=0
rmiregistry > 42749 [SYN, ACK, ECN]
Seq=3749262223 Ack=3740552480 Win=32767 Len=0
42749 > rmiregistry [ACK] Seq=3740552480 Ack=3749262224 Win=32767 Len=0
JRMI, Version: 2, StreamProtocol
rmiregistry > 42749 [ACK] Seq=3749262224 Ack=3740552487 Win=32767 Len=0
JRMI, ProtocolAck
42749 > rmiregistry [ACK] Seq=3740552487 Ack=3749262240 Win=32767 Len=0
Continuation
rmiregistry > 42749 [ACK] Seq=3749262240 Ack=3740552506 Win=32767 Len=0
JRMI, Call
rmiregistry > 42749 [ACK] Seq=3749262240 Ack=3740552556 Win=32767 Len=0
JRMI, ReturnData
42749 > rmiregistry [ACK] Seq=3740552556 Ack=3749262442 Win=32767 Len=0
JRMI, Ping
JRMI, PingAck
42749 > rmiregistry [ACK] Seq=3740552557 Ack=3749262443 Win=32767 Len=0
JRMI, DgcAck
42749 > rmiregistry [FIN, ACK]
Seq=3740552572 Ack=3749262443 Win=32767 Len=0
rmiregistry > 42749 [FIN, ACK]
Seq=3749262443 Ack=3740552573 Win=32767 Len=0
42749 > rmiregistry [ACK] Seq=3740552573 Ack=3749262444 Win=32767 Len=0
        实验的结果是:RMI与TCP based socket相比,传输相同的有效数据,RMI需要占用更多的网络带宽(protocol overhead)。从这里,我们可以得出一个一般性的结论:RMI主要是用于远程方法的”调用“(RMI是多么的名符其实:)),其技术内涵强调的是“调用”,基于此,我能想到的是:移动计算,和远程控制,当你的应用不需要在client与server之间传输大量的数据时,RMI是较好的选择,它简洁、易于开发。但是,一旦你的应用需要在client与server之间传输大量的数据,极端的,比如FTP应用,则RMI是不适合的,我们应该使用socket。

四、参考资料:
Network-programming with RMI, by Adrian Reber, URL:
http://42.fht-esslingen.de/~adrian/master/rmi.pdf
posted @ 2007-07-28 19:06 爱上龙卷风 阅读(1296) | 评论 (2)编辑 收藏

2007年7月22日 #

     摘要: 在windows平台下,用于对多线程(包括进程)之间的同步保护机制,基本上有这么几种:
1)Critical Section对象 2)Event对象 3)Mutext 对象 4) Semaphore对象。网上已经有很多的文章在介绍这些对象是怎么使用的。本文的着眼点在于:总结出这些同步保护机制的一些明显的行为特征,而这些行为特征,也是我们再写程序时经常会碰到的。  阅读全文
posted @ 2007-07-22 21:10 爱上龙卷风 阅读(1323) | 评论 (3)编辑 收藏

2007年7月15日 #

不知道大家是否有同感,在开发软件应用系统的过程中我们经常面临一个非常tedious的问题,即如何分类和处理软件行为产生的错误值,大致有这样几个方面的问题:
        第一.错误值如何分类。参考microsoft的开放文档,按照错误值的严重程度分为critical、error、warning、information等四个级别。
        第二.错误值如何在各个软件子模块中有效的表示和沟通。
        第三.如何划分出用户所关心的错误值集合。
这篇文章对错误类型定义与处理作了很好的阐释。推荐给大家,大家如果有什么好的开发经验,欢迎探讨。
FYI:
Taxonomy of Error Types and Error Handling
Posted: Jun 21, 2007 8:13 PM

Most of what is written about error and exception handling is fairly abstract and vague. When specific recommendations are made, those usually consist of examples given for specific circumstances. Yet, error handling is a fundamental task for developers. This brief review attempts to categorize the different types of error conditions a program can encounter. By describing these categories, I also provide suggestions on how to handle each type of error condition.

In general, errors a program can encounter tend to be the result of one of three things:

  • Restrictions: Arguments to a routine that can never work, and always result in an error, define a restriction on the use of the routine. Ensuring that only correct arguments are passed to a routine is the type of thing that programming by contract is meant to address.
  • Inconsistencies: When values or resources are not what they are expected to be, or are missing, that creates an inconsistency between the expected state of the environment and the actual state. This may be the internal environment, such as a null pointer, or the external environment, such as a corrupt file. It doesn't encompass inconsistencies in the data model, which often needs to be temporarily inconsistent during an operation (e.g. adding a node to a linked list).
  • Failures When an operation simply does not work, and it's out of the program's control, this is a failure. For example, a pulled network cable.

These types of errors overlap to an extent (say, a working network could be considered part of the expected state, making it an inconsistency error). In general, though, most errors can fall into one of these categories.

Sometimes failures are not errors, and are just a way of detecting the current external state. For example, opening a file that doesn't exist may fail, but results in an error only when that file is actually needed.

Error Handling Responsibilities

Program code is responsible for the consistency of the internal program state. Generally certain code has primary (ideally, exclusive) responsibility for parts of the internal state. Inconsistency errors that occur within the code responsible for that state are bugs.

Sometimes the state management responsibility is shared between different sections of code. This is a bad idea, because it makes assigning responsibility for an inconsistency error harder, but it does happen in practice.

It's important to make a distinction between error detection and debugging. Often, data generated in the process of error handling is mixed together with diagnostic information. If possible, these types of information should be kept completely separate—at least conceptually, even if combined in a single data structure.

Safe Zones

Restrictions can be checked before calling a routine, or within a routine. It seems a waste of time to check arguments every time a routine is called when you already know those arguments are correct. One strategy is to separate parameter checking from parameter usage. This doesn't work reliably for library code, where anything can happen between the check and the use of the parameters, but within a base of code for a particular application or within a library, you can restrict the code to not change a value known to be safe.

The code between a parameter check and the next change to a parameter variable is a safe zone, where parameters don't have to be re-checked. This is only valid for restriction errors, because inconsistency and failure errors can be caused by things outside the code's safe zone. Things like critical sections (in multithreaded environments), semaphores and file locks are meant to create a very limited kind of safe zone for inconsistency and failure errors.

The code safe zones for parameters can overlap with others, and may not be well defined. One way to deal with this is to assign known safe values to variables which indicate this safety. Joel Spolsky wrote about one way to do this using variable naming conventions in Making Wrong Code Look Wrong. Safe values should be assigned to variables declared constant.

Reporting Errors

Code calling a routine needs to know three things to decide how to proceed: First, whether the data is returned, if any, or if the method invocation succeeded; second, whether an error occurred; and, third, whether the error is permanent or transitory. This defines the following possible error states returned from a routine:

  1. Successful
  2. Restriction error (always permanent)
  3. Permanent (bug) inconsistency
  4. Transitory (detected) inconsistency
  5. Failure (transitory for all we know)

It's often a bad idea to mix an error code with a return value, such as designating a specific values—say, 0 or -1—to be invalid. Some languages, like Python, allow multiple values to be returned from a method as tuples. A tuple is basically an anonymous class, and can be implemented in a language like Java by defining a class for objects returned by a method, or in C by defining a struct which is passed as a parameter and is updated by the function. But in many cases, exceptions are a much better way to separate error information from return values.

Exceptions transmit an object from the exception location to a handler in a scope surrounding it, or surrounding the point where the routine was called. The exception objects include information by the object type and class, and debugging information the data contained within that type. Exceptions by themselves don't indicate the error state, so that must be included as an attribute of the exception object, or the error state must be deduced from the debugging information (object type and data).

Java introduced the controversial notion of checked exceptions, which must either be caught or declared to be thrown by a method in order to compile, while unchecked (or runtime) exceptions behave like exceptions in other languages. The main cause of the controversy is that there has been no good definition of why there should be a difference and, as a result, no consistent strategy in the implementation in various libraries, including standard parts of the different Java runtime libraries.

In general, unchecked exceptions are meant for bugs, where an error indicates that the code is simply wrong and must be fixed (restriction and bug inconsistency errors). An example is a NullPointerException. Checked exceptions are for detected inconsistency and failure errors, where the program may have a strategy of handling the error. An example is an I/O error.

Transactional Operations

One strategy to handle errors is to make all operations transactional, so that if they fail, it's as if the operation was never tried. One way implement this is to define an "undo" operation for every change:

Ideal transactional function

In this example, the functions are also transactional, and thus don't need to be rolled back if they fail. This can be done with nested if/else blocks, or with nested try/catch blocks. If the "undo" operations themselves have errors, the result looks more like this:

Realistic transactional function

One way of dealing with this is to modify a copy of the program state, and if all operations succeed, only then commit the changes. The commit may fail, but this isolates the possible state changing errors to one point, and is similar how databases implement transactions. Another way to implement transactional operations is to make a copy of before any state is changed, and use that copy to restore the expected state, in case of an error.

In summary, having a clear taxonomy of error conditions that code may encounter helps develop better strategies for dealing with, and possibly recovering from, those errors.

posted @ 2007-07-15 18:31 爱上龙卷风 阅读(601) | 评论 (0)编辑 收藏

2007年7月1日 #

一、异步IO
        对于应用程序而言,有两种类型的IO调用:即同步IO与异步IO。其本质的区别是:同步IO会block当前的调用线程,而异步IO则允许发起IO请求的调用线程继续执行,等到IO请求被处理后,会通知调用线程。在windows平台上,应用程序可以调用CreateFile API, 并通过设置FILE_FLAG_OVERLAPPED标志来决定是否发起异步IO请求。
        对于异步的IO请求,其最大的好处是:慢速的IO请求相对于应用程序而言是异步执行,这样可以极大提高应用程序的处理吞吐量。发起IO请求的应用程序需要关心的是IO执行完成的结果,而不必忙等IO请求执行的过程。
       事实上,无论对于同步IO,还是异步IO,当IO请求发送到device driver后,device driver的执行总是异步的,当它接到IO请求之后,总会马上返回给IO System。而IO System是否立即返回给调用线程,则取决于FILE_FLAG_OVERLAPPED标志的设置,如下图:


二、异步IO的同步问题。
        我们使用异步IO,是为了提高应用程序的处理吞吐量。但是,当异步IO不再异步时(无论你是否设置FILE_FLAG_OVERLAPPED标志),应用程序的性能会受到极大的影响。根据Microsoft Knowledge Base 156932, 在下列几种情况下,异步IO会失去它的异步性,而表现出同步的性质:
1)如果文件使用了NTFS compression压缩,则system driver不会异步地存取这样的文件。
2)扩展文件长度的IO操作不会是异步操作。
3)Cache机制。如果IO操作使用了file system cache,则这样的IO操作会被当成同步IO,而非异步IO。
即使你使用了FILE_FLAG_OVERLAPPED标志。在这种情况下,
a.如果需要读取的数据已经在Cache里,那么I/O drivers会认为这样的IO请求可以被立即处理,其结果是ReadFile 或者WriteFile调用返回TRUE,表示是:同步处理完成。
b.如果需要读取的数据不在Cache里,windows NT的file system是使用page-faulting机制来实现cache管理,而page-faulting总是被同步处理, Windows NT没有提供异步的page-faulting机制。的确, file system driver使用了线程池来缓解这一问题,但是,当应用程序发起的IO请求足够多时,线程池还是不能应付的。
        在我们开发基于异步IO应用程序时,应该避免上述问题的出现,因为它们会使程序的性能大打折扣。
那么,对于Cache,我们如何避免呢?答案是:请使用FILE_FLAG_NO_BUFFERING标志。这个标志会使异步IO真实地异步执行。
三、性能的测试数据(仅供参考)。
      我在我的机器上,简单地对使用FILE_FLAG_NO_BUFFERING标志的异步IO,与不使用FILE_FLAG_NO_BUFFERING标志的异步IO进行了对比。
操作:顺序读取1G的文件。
x轴表示:每次读取的字节数(单位:K/每次)
Y轴表示:读取完成所需要的时间。(单位:millisecond)
注意:每次测试读取的内容总数是相等的(1000M)。
例如:如果每次读取128k,则需要读取8000次(128k*8000 = 1000M)。
如果每次读取256k,则需要读取4000次(256k*4000 = 1000M)。
粉红色的线没有使用FILE_FLAG_NO_BUFFERING标志,而黄色的线使用了FILE_FLAG_NO_BUFFERING标志。

从以上的数据,我们可以得出以下结论:
1) 当使用FILE_FLAG_NO_BUFFERING标志,应用程序的性能会极大提高,大概有50%的提高。
2)在使用异步IO的时候,还有一个注意的问题是:当你每次读取的字节数增大的时候,性能也会提高。尤其在小于1024k时,当增大次读取的字节数,性能都有明显的提高。在混合了网络传输等复杂因素的应用程序开发过程中,建议将该值设置为可配置的参数,通过调整该参数,使你的应用达到最好的性能。

参考资料:

1) Microsoft Knowledge Base 156932

2)  Microsoft Windows Internals, Fourth Edition. 

posted @ 2007-07-01 18:45 爱上龙卷风 阅读(2860) | 评论 (5)编辑 收藏

仅列出标题  下一页