BackBufferWidth:全屏幕式后备缓冲的宽度
BackBufferHeight :全屏幕式后备缓冲的高度
BackBufferFormat: 后备缓冲的格式
BackBufferCount: 后备缓冲数目
MultiSampleType: 全屏抗锯齿类型
MultiSampleQuality: 全屏抗锯齿质量等级
SwapEffect: 交换缓冲类型
hDeviceWindow: 设备窗口句柄
Windowed: 全屏或窗口
EnableAutoDepthStencil: 激活深度缓冲
AutoDepthStencilFormat: 深度缓冲格式
FullScreen_RefreshRateInHz: 显示器刷新率
PresentationInterval: 图像最大刷新速度

 

typedef struct _D3DPRESENT_PARAMETERS_ {
    UINT BackBufferWidth, BackBufferHeight;
    D3DFORMAT BackBufferFormat;
    UINT BackBufferCount;
    D3DMULTISAMPLE_TYPE MultiSampleType;
    DWORD MultiSampleQuality;
    D3DSWAPEFFECT SwapEffect;
    HWND hDeviceWindow;
    BOOL Windowed;
    BOOL EnableAutoDepthStencil;
    D3DFORMAT AutoDepthStencilFormat;
    DWORD Flags;
    UINT FullScreen_RefreshRateInHz;
    UINT PresentationInterval;
} D3DPRESENT_PARAMETERS;

BackBufferWidth:全屏幕式后备缓冲的宽度
BackBufferHeight :全屏幕式后备缓冲的高度
BackBufferFormat: 后备缓冲的格式
BackBufferCount: 后备缓冲数目
MultiSampleType: 全屏抗锯齿类型
MultiSampleQuality: 全屏抗锯齿质量等级
SwapEffect: 交换缓冲类型
hDeviceWindow: 设备窗口句柄
Windowed: 全屏或窗口
EnableAutoDepthStencil: 激活深度缓冲
AutoDepthStencilFormat: 深度缓冲格式
FullScreen_RefreshRateInHz: 显示器刷新率
PresentationInterval: 图像最大刷新速度

BackBufferWidth和BackBufferHeight:后备缓冲的宽度和高度。在全屏模式下,这两者的值必需符合显卡所支持的分辨率。例如(800,600),(640,480)。
BackBufferFormat:后备缓冲的格式。这个参数是一个D3DFORMAT枚举类型,它的值有很多种,例如D3DFMT_R5G6B5、D3DFMT_X8R8G8B8为游戏后备缓冲常用格式,这说明后备缓冲的格式是每个像素16位,其实红色(R)占5位,绿色(G)占6位,蓝色(B)占5位,为什么绿色会多一位呢?据说是因为人的眼睛对绿色比较敏感。DX9只支持16位和32位的后备缓冲格式,24位并不支持。如果对这D3DFORMAT不熟悉的话,可以把它设为D3DFMT_UNKNOWN,这时候它将使用桌面的格式。

BackBufferCount:后备缓冲的数目,范围是从0到3,如果为0,那就当成1来处理。大多数情况我们只使用一个后备缓冲。使用多个后备缓冲可以使画面很流畅,但是却会造成输入设备响应过慢,还会消耗很多内存。
MultiSampleType 和MultiSampleQuality:前者指的是全屏抗锯齿的类型,后者指的是全屏抗锯齿的质量等级。这两个参数可以使你的渲染场景变得更好看,但是却消耗你很多内存资源,而且,并不是所有的显卡都支持这两者的所设定的功能的。在这里我们分别把它们设为D3DMULTISAMPLE_NONE和0。
可以通过CheckDeviceMultiSampleType 来检测当前装置是否支持某个抗锯齿类型
typedef enum _D3DMULTISAMPLE_TYPE
{
    D3DMULTISAMPLE_NONE               = 0,
    D3DMULTISAMPLE_2_SAMPLES       = 2,
    D3DMULTISAMPLE_3_SAMPLES       = 3,
    D3DMULTISAMPLE_4_SAMPLES       = 4,
    D3DMULTISAMPLE_5_SAMPLES       = 5,
    D3DMULTISAMPLE_6_SAMPLES       = 6,
    D3DMULTISAMPLE_7_SAMPLES       = 7,
    D3DMULTISAMPLE_8_SAMPLES       = 8,
    D3DMULTISAMPLE_9_SAMPLES       = 9,
    D3DMULTISAMPLE_10_SAMPLES      = 10,
    D3DMULTISAMPLE_11_SAMPLES      = 11,
    D3DMULTISAMPLE_12_SAMPLES      = 12,
    D3DMULTISAMPLE_13_SAMPLES      = 13,
    D3DMULTISAMPLE_14_SAMPLES      = 14,
    D3DMULTISAMPLE_15_SAMPLES      = 15,
    D3DMULTISAMPLE_16_SAMPLES      = 16,
    D3DMULTISAMPLE_FORCE_DWORD     = 0x7fffffff
} D3DMULTISAMPLE_TYPE;
//type 为以上的枚举值
D3DMULTISAMPLE_TYPE type;
int quality; //获取相对于type的最大质量等级(设置type抗拒值类型时,设置的质量等级只能小于这个数)
//m_d3dBackFmt 为后备缓冲格式,m_bWindowed 是否为窗口模式,
m_pD3D9->CheckDeviceMultiSampleType( D3DADAPTER_DEFAULT,D3DDEVTYPE_HAL,m_d3dBackFmt, m_bWindowed,type,&quality );

SwapEffect:交换缓冲支持的效果类型,指定表面在交换链中是如何被交换的。它是D3DSWAPEFFECT枚举类型,可以设定为以下三者之一:D3DSWAPEFFECT_DISCARD,D3DSWAPEFFECT_FLIP,D3DSWAPEFFECT_COPY。
如果设定为D3DSWAPEFFECT_DISCARD,则后备缓冲区的东西被复制到屏幕上后,后备缓冲区的东西就没有什么用了,可以丢弃(discard 是否丢弃由显卡决定,但不再等待)了。
如果设定为D3DSWAPEFFECT_FLIP,后备缓冲拷贝到前台缓冲,保持后备缓冲内容不变。当后备缓冲大于1个时使用
设定D3DSWAPEFFECT_COPY的话, 后备缓冲拷贝到前台缓冲,保持后备缓冲内容不变。当后备缓冲等于1个时使用
一般我们是把这个参数设为D3DSWAPEFFECT_DISCARD。如果想使用GetBackBuffer 获得后备缓冲内容打印屏幕画面。则不能使用DISCARD.
很怀疑用DISCARD效率会好的说法。在我的8600GT 上_COPY 明显好于DISCARD. discard 做法是再次使用后备缓冲时 new 一个新的缓冲,旧的缓冲内容遗弃,如果还有使用旧的缓冲地方,不会影响新的内容,如使用抗锯齿必须是DISCARD。这样不用等待硬件同步。不过大部分是一次Present操作。用COPY在新机器上面反倒效率好些。毕竟new 一个 1440*900 的后台缓冲也是有消耗的。(对于DISCARD 做法仅仅是猜测。不同显卡可能不同。)
hDeviceWindow:显示设备输出窗口的句柄
Windowed:如果为FALSE,表示要渲染全屏。如果为TRUE,表示要渲染窗口。渲染全屏的时候,BackBufferWidth和BackBufferHeight的值就得符合显示模式中所设定的值。
EnableAutoDepthStencil:如果要使用Z缓冲或模板缓冲,则把它设为TRUE。
AutoDepthStencilFormat:如果不使用深度缓冲,那么这个参数将没有用。如果启动了深度缓冲,那么这个参数将为深度缓冲设定缓冲格式。常用值D3DFMT_24S8 (24 深度缓冲,8模板缓冲),D3DFMT_24X8(24 深度缓冲),D3DFMT_16(16深度缓冲)等等。
//深度缓存和模板缓存的象素格式,如 D3DFMT_D24S8 , 24 位表示深度,8位为模板缓存。一般不会用到32位深度:D3DFMT_32
//注意如果设置模板缓冲在Clear() 函数中也要清楚模板缓冲:设置参数 D3DCLEAR_ZBUFFER|D3DCLEAR_STENCIL

Flags:通常为0 或D3DPRESENTFLAG_LOCKABLE_BACKBUFFER。不太清楚是用来做什么的,看字面好像是一个能否锁定后备缓冲区的标记。D3DPRESENTFLAG_DISCARD_DEPTHSTENCIL 丢弃模板缓冲区
FullScreen_RefreshRateInHz:显示器的刷新率,单位是HZ,如果设定了一个显示器不支持的刷新率,将会不能创建设备或发出警告信息。为了方便,一般设为D3DPRESENT_RATE_DEFAULT就行了。
PresentationInterval:如果设置为D3DPRENSENT_INTERVAL_DEFAULT,则说明在显示一个渲染画面的时候必要等候显示器刷新完一次屏幕。例如你的显示器刷新率设为80HZ的话,则一秒内你最多可以显示80个渲染画面。另外你也可以设置在显示器刷新一次屏幕的时间内显示1到4个画面。如果设置为 D3DPRENSENT_INTERVAL_IMMEDIATE,则表示可以以实时的方式来显示渲染画面,虽然这样可以提高帧速(FPS),如果速度过快却会产生图像撕裂的情况,但当游戏完成时,帧速度一般不会过快。

posted @ 2010-07-09 10:03 小火球 阅读(492) | 评论 (0)编辑 收藏

C语言中的main函数,一般会带有2个参数,例如int main ( int argc, char* argv[]),这是一个典型的main函数的声明。这是为了在执行程序时需要向程序传递参数,参数argc代表了输入参数的个数,char *argv[]表示传入的参数的字符串,是一个字符串数组。

例如在Unix平台下编写一个小程序:
int main(int argc, char* argv[])
{
 int i;

 printf("test main parameter\n");
 printf("argc:%d\n", argc);
 for(i=0;i<argc;i++)
 {
  printf("argv[%d]:%s\n", i, argv[i]);
 }
 exit(0);
}
用cc编译后形成一个a.out*的可执行的文件,运行a.out,其结果是argc=1,argv[0]="a.out",运行的程序的文件名,也占用一个参数位置,也就是说argv数组中的第一个单元指向的字符串总是
可执行程序的名字,以后的单元指向的字符串依次是程序调用时的参数。这个赋值过程时操作系统完成的,我们只需要拿来用就可以了。
 

main()主函数


    每一C 程序都必须有一main()函数, 可以根据自己的爱好把它放在程序的某
个地方。有些程序员把它放在最前面, 而另一些程序员把它放在最后面, 无论放
在哪个地方, 以下几点说明都是适合的。
    1. main() 参数
    在Turbo C2.0启动过程中, 传递main()函数三个参数: argc, argv和env。
     * argc:  整数, 为传给main()的命令行参数个数。
     * argv:  字符串数组。
              在DOS 3.X 版本中, argv[0] 为程序运行的全路径名; 对DOS 3.0
              以下的版本, argv[0]为空串("") 。
              argv[1] 为在DOS命令行中执行程序名后的第一个字符串;
              argv[2] 为执行程序名后的第二个字符串;
              ...
              argv[argc]为NULL。
     *env:  安符串数组。env[] 的每一个元素都包含ENVVAR=value形式的字符
串。其中ENVVAR为环境变量如PATH或87。value 为ENVVAR的对应值如C:\DOS, C:
\TURBOC(对于PATH) 或YES(对于87)。


   
    Turbo C2.0启动时总是把这三个参数传递给main()函数, 可以在用户程序中
说明(或不说明)它们, 如果说明了部分(或全部)参数, 它们就成为main()子程序
的局部变量。
    请注意: 一旦想说明这些参数, 则必须按argc, argv, env 的顺序, 如以下
的例子:
     main()
     main(int argc)
     main(int argc, char *argv[])
     main(int argc, char *argv[], char *env[])
    其中第二种情况是合法的, 但不常见, 因为在程序中很少有只用argc, 而不
用argv[]的情况。
    以下提供一样例程序EXAMPLE.EXE,  演示如何在main()函数中使用三个参数:
     /*program name EXAMPLE.EXE*/
     #include <stdio.h>
     #include <stdlib.h>
     main(int argc, char *argv[], char *env[])
     {
          int i;
          printf("These are the %d  command- line  arguments passed  to
                  main:\n\n", argc);
          for(i=0; i<=argc; i++)
            printf("argv[%d]:%s\n", i, argv
);
          printf("\nThe environment string(s)on this system are:\n\n");
          for(i=0; env
!=NULL; i++)
               printf(" env[%d]:%s\n", i, env
);
     }




   

    如果在DOS 提示符下, 按以下方式运行EXAMPLE.EXE:
    C:\example first_argument "argument with blanks"  3  4  "last  but
one" stop!
    注意: 可以用双引号括起内含空格的参数, 如本例中的:   "  argument
with blanks"和"Last but one")。
    结果是这样的:
     The value of argc is 7
     These are the 7 command-linearguments passed to main:
     argv[0]:C:\TURBO\EXAMPLE.EXE
     argv[1]:first_argument
     argv[2]:argument with blanks
     argv[3]:3
     argv[4]:4
     argv[5]:last but one
     argv[6]:stop!
     argv[7]:(NULL)
     The environment string(s) on this system are:
     env[0]: COMSPEC=C:\COMMAND.COM
     env[1]: PROMPT=$P$G            /*视具体设置而定*/
     env[2]: PATH=C:\DOS;C:\TC      /*视具体设置而定*/
 
     应该提醒的是: 传送main() 函数的命令行参数的最大长度为128 个字符 (包
括参数间的空格),  这是由DOS 限制的。

 
 

posted @ 2010-06-28 12:03 小火球 阅读(339) | 评论 (0)编辑 收藏

 

在我的博客之前写了很多关于IOCP的“行云流水”似的看了让人发狂的文章,尤其是几篇关于IOCP加线程池文章,更是让一些功力不够深厚的初学IOCP者,有种吐血的感觉。为了让大家能够立刻提升内力修为,并且迅速的掌握IOCP这个Windows平台上的乾坤大挪移心法,这次我决定给大家好好补补这个基础。

要想彻底征服IOCP,并应用好IOCP这个模型,首先就让我们穿越到遥远的计算机青铜器时代(以出现PC为标志),那时候普通的PC安装的还是DOS平台,微软公司主要靠这个操作系统在IT界的原始丛林中打拼,在DOS中编写程序,不得不与很多的硬件直接打交道,而最常操作的硬件无非是键盘、声显卡、硬盘等等,这些设备都有一个特点就是速度慢,当然是相对于PC平台核心CPU的速度而言,尤其是硬盘这个机械电子设备,其速度对于完全电子化得CPU来说简直是“相对静止”的设备。很多时候CPU可以干完n件(n>1000)事情的时间中,这些硬件可能还没有完成一件事情,显然让CPU和这些硬件同步工作将是一种严重的浪费,并且也不太可能,此时,聪明的硬件设计师们发明了一种叫做中断的操作方式,用以匹配这种速度上的严重差异。中断工作的基本原理就是,CPU首先设置一个类似回调函数的入口地址,其次CPU对某个硬件发出一个指令,此时CPU就去干别的活计了,最后那个慢的象蜗牛一样的硬件执行完那个指令后,就通知CPU,让CPU暂时“中断”手头的工作,去调用那个“回调函数”。至此一个完整的中断调用就结束了。这个模型曾经解决了显卡与CPU不同步的问题,最重要的是解决了硬盘速度与CPU速度严重不匹配的问题,并因此还派生出了更有名的DMA(直接内存访问技术,主要是指慢速硬件可以读写原本只能由CPU直接读写的内存)硬盘IO方式。(注意这里说的中断工作方式只是中断工作方式的一种,并不是全部,详细的中断原理请参阅其它专业文献。)

其实“中断”方式更像是一种管理模型,比如在一个公司中,如果要老板时时刻刻盯着员工作事情,那么除非是超人,否则无人能够胜任,同时对于老板这个稀缺资源来说也是一种极起严重的浪费。更多时候老板只是发指令给员工,然后员工去执行,而老板就可以做别的事情,或者干脆去打高尔夫休息,当员工完成了任务就会通过电话、短信、甚至e-mail等通知老板,此时老板就去完成一个响应过程,比如总结、奖罚、发出新指令等等。由此也看出如果一个公司的“老板占用率”(类似CPU占用率)太高,那么就说明两种情况:要么是它的员工很高效,单位时间内完成的指令非常多;要么是公司还没有建立有效的“中断”响应模型。如果你的公司是后者,那么你就可以试着用这个模型改造公司的管理了,由此你可以晋升到管理层,而不用再去管你的服务端程序有没有使用IOCP了,呵呵呵。

如果真的搞明白了这个传说中的“中断”操作方式,那么理解IOCP的基本原理就不费劲了。

结束了计算机的青铜时代后,让我们穿越到现在这个“计算机蒸汽”时代,(注意不是“计算机IT”时代,因为计算机还没法自己编写程序让自己去解决问题)。在现代,Windows几乎成了PC平台上的标准系统,而PC平台上的几大件还是没有太大的变化,除了速度越来越快。而因为操作系统的美妙封装,我们也不用再去直接同硬件打交道了,当然编写驱动程序的除外。

在Windows平台上,我们不断的调用着WriteFile和ReadFile这些抽象的函数,操作着“文件”这种抽象的信息集合,很多时候调用这些函数时,是以一种“准同步”的方式操作硬件的,比如要向一个文件中写入1M的信息,只有等到WriteFile函数返回,操作才算结束,这个过程中,我们的程序则类似死机一样,等待硬盘写入操作的结束(实际是被系统切换出了当前的CPU时间片)。于此同时,调用了WriteFile的线程则无法干别的任何事情。因为整个线程是在以一种称为过程化的模型中运行,所有的处理流程全部是线性的。对于程序的流畅编写来说,线性化的东西是一个非常好的东西,甚至几乎早期很多标准的算法都是基于程序是过程化得这一假设而设计的。而对于一些多任务、多线程环境来说,这种线性的工作方式会使系统严重低效,甚至造成严重的浪费,尤其在现代多核CPU已成为主流的时候,显然让一个CPU内核去等待另一个CPU内核完成某事后再去工作,是非常愚蠢的一种做法。

面对这种情况,很多程序员的选择是多线程,也就是专门让一个线程去进行读写操作,而别的线程继续工作,以绕开这些看起来像死机一样的函数,但是这个读写线程本身还是以一种与硬盘同步的方式工作的。然而这并不是解决问题的最终方法。我们可以想象一个繁忙的数据库系统,要不断的读写硬盘上的文件,可能在短短的一秒钟时间就要调用n多次WriteFile或ReadFile,假设这是一个网站的后台数据库,那么这样的读写操作有时还可能都是较大的数据块,比如网站的图片就是比较典型的大块型数据,这时显然一个读写线程也是忙不过来的,因为很有可能一个写操作还没有结束,就会又有读写操作请求进入,这时读写线程几乎变成了无响应的一个线程,可以想象这种情况下,程序可能几乎总在瘫痪状态,所有其它的线程都要等待读写操作线程完活。也许你会想多建n个线程来进行读写操作,其实这种情况会更糟糕,因为不管你有多少线程,先不说浪费了多少系统资源,而你读写的可能是相同的一块硬盘,只有一条通道,结果依然是一样的,想象硬盘是独木桥,而有很多人(线程)等着过桥的情形,你就知道这更是一个糟糕的情形。所以说在慢速的IO面前,多线程往往不是“万灵丹”。

面对这种情形,微软公司为Windows系统专门建立了一种类似“青铜时代”的中断方式的模型来解决这个问题。当然,不能再像那个年代那样直接操作硬件了,需要的是旧瓶装新酒了。微软是如何做到的呢,实际还是通过“回调函数”来解决这个问题的,大致也就是要我们去实现一个类似回调函数的过程,主要用于处理来自系统的一些输入输出操作“完成”的通知,相当于一个“中断”,然后就可以在过程中做输入输出完成的一些操作了。比如在IO操作完成后删除缓冲,继续发出下一个命令,或者关闭文件,设备等。实际上从逻辑的角度来讲,我们依然可以按照线性的方法来分析整个过程,只不过这是需要考虑的是两个不同的函数过程之间的线性关系,第一个函数是发出IO操作的调用者,而第二个函数则是在完成IO操作之后的被调用者,。而被调用的这个函数在输入输出过程中是不活动的,也不占用线程资源,它只是个过程(其实就是个函数,内存中的一段代码而已)。调用这些函数则需要一个线程的上下文,实际也就是一个函数调用栈,很多时候,系统会借用你进程空间中线程来调用这个过程,当然前提条件是事先将可以被利用的线程设置成“可警告”状态,这也是线程可警告状态的全部意义,也就是大多数内核同步等待函数bAlertable(有些书翻译做可警告的,我认为应该理解为对IO操作是一种“时刻警惕”的状态)参数被传递TRUE值之后的效果。比如:WaitForSingleObjectEx、SleepEx等等。

当然上面说的这种方式其实是一种“借用线程”的方式,当进程中没有线程可借,或者可借的线程本身也比较忙碌的时候,会造成严重的线程争用情况,从而造成整体性能低下,这个方式的局限性也就显现出来了。注意“可警告”状态的线程,并不总是在可以被借用的状态,它们本身往往也需要完成一些工作,而它调用一些能够让它进入等待状态的函数时,才可以被系统借用,否则还是不能被借用的。当然借用线程时因为系统有效的保护了栈环境和寄存器环境,所以被借用的线程再被还回时线程环境是不会被破坏的。

鉴于借用的线程的不方便和不专业,我们更希望通过明确的“创建”一批专门的线程来调用这些回调函数(为了能够更深入的理解,可以将借用的线程想象成出租车,而将专门的线程想象成私家车),因此微软就发明了IOCP“完成端口”这种线程池模型,注意IOCP本质是一种线程池的模型,当然这个线程池的核心工作就是去调用IO操作完成时的回调函数,这就叫专业!这也是IOCP名字的来由,这就比借用线程的方式要更加高效和专业,因为这些线程是专门创建来做此工作的,所以不用担心它们还会去做别的工作,而造成忙碌或不响应回调函数的情况,另外因为IO操作毕竟是慢速的操作,所以几个线程就已经足可以应付成千上万的输入输出完成操作的请求了(还有一个前提就是你的回调函数做的工作要足够少),所以这个模型的性能是非常高的。也是现在Windows平台上性能最好的输入输出模型。它首先就被用来处理硬盘操作的输入输出,同时它也支持邮槽、管道、甚至WinSock的网络输入输出。

至此对于完成端口的本质原理应该有了一个比较好的理解,尤其是掌握了IOCP是线程池模型的这一本质,那么对于之后的IOCP实际应用就不会有太多的疑问了。接下去就让我们从实际编程的角度来了解一下IOCP,也为彻底掌握IOCP编程打下坚实的基础。

要应用IOCP,首先就要我们创建一个叫做IOCP的内核对象,这需要通过CreateIoCompletionPort这个函数来创建,这个函数的原型如下:

HANDLE WINAPI CreateIoCompletionPort(

__in          HANDLE FileHandle,

__in          HANDLE ExistingCompletionPort,

__in          ULONG_PTR CompletionKey,

__in          DWORD NumberOfConcurrentThreads

);

这个函数是个本身具有多重功能的函数(Windows平台上这样的函数并不多),需要用不同的方式来调用,以实现不同的功能,它的第一个功能正如其名字所描述的“Create”,就是创建一个完成端口的内核对象,要让他完成这个功能,只需要指定NumberOfConcurrentThreads参数即可,前三个参数在这种情况下是没有意义的,只需要全部传递NULL即可,象下面这样我们就创建了一个完成端口的内核对象:

HANDLE hICP = ::CreateIoCompletionPort(INVALID_HANDLE_VALUE,NULL,0,1);

这里首先解释下为什么第一个参数不是NULL而是INVALID_HANDLE_VALUE,因为第一个参数按照定义是一个文件的句柄,也就是需要IOCP操作的文件句柄,而代表“NULL”文件句柄的实际值是INVALID_HANDLE_VALUE,这是因为NULL实际等于0,而0这个文件句柄被用于特殊用途,所以要用INVALID_HANDLE_VALUE来代表“NULL”意义的文件,INVALID_HANDLE_VALUE的值是-1或者0xFFFFFFFF。

最后一个参数NumberOfConcurrentThreads就有必要好好细细的说说了,因为很多文章中对于这个参数总是说的含糊其辞,不知所云,有些文章中甚至人云亦云的说赋值为CPU个数的2倍即可,所谓知其然,不知其所以然。其实这个参数的真实含义就是“真正并发同时执行的最大线程数”,这个并发是真并发,怎么去理解呢,如果你有两颗CPU,而你赋值为2那么就是说,在每颗CPU上执行一个线程,并且真正的并发同时执行,当然如果你设置了比CPU数量更大的数值,它的含义就变成了一个理论并发值,而实际系统的最大可能的严格意义上的并发线程数就是CPU个数,也就是你在任务管理器中看到的CPU个数(可能是物理个数,也可能是内核个数,还有可能是超线程个数,或者它们的积)。讲到这里大家也许就有疑问了,为什么有些文章资料中说要设置成CPU个数的2倍呢?这通常是一个半经验值,因为大多数IOCP完成回调的过程中,需要一些逻辑处理,有些是业务性的,有些要访问数据库,有些还可能访问硬盘,有些可能需要进行数据显示等等,无论哪种处理,这总是要花费时间的,而系统发现你设置了超过CPU个数的并发值时,那么它就尽可能的来回切换这些线程,使他们在一个时间段内看起来像是并发的,比如在1ms的时间周期内,同时有4个IOCP线程被调用,那么从1ms这段时间来看的话,可以认为是有4个线程被并发执行了,当然时间可以无限被细分,真并发和模拟并发实际就是针对时间细分的粒度来说的。这样一来如何设置并发数就是个设计决策问题,决策的依据就是你的回调函数究竟要干些什么活,如果是时间较长的活计,就要考虑切换其它线程池来完成,如果是等待性质的活计,比如访问硬盘,等待某个事件等,就可以设置高一点的并发值,强制系统切换线程造成“伪并发”,如果是非常快速的活计,那么就直接设置CPU个数的并发数就行了,这时候防止线程频繁切换是首要任务。当然并发数最好是跟踪调试一下后再做决定,默认的推荐值就是CPU个数的2倍了。(绕了一大圈我还是“人云亦云”了一下,哎呦!谁扔的砖头?!)

上面的全部就是创建一个完成端口对象,接下来就是打造线程了,打造的方法地球人都知道了,就是CreateThread,当然按照人云亦云的说法应该替之以_beginthread或_beginthreadex,原因嘛?你想知道?真的想知道?好了看你这么诚恳的看到了这里,那就告诉你吧,原因其实就是因为我们使用的语言从本质上说是C/C++,很多时候我们需要在线程函数中调用很多的C/C++味很重的库函数,而有些函数是在Windows诞生以前甚至是多线程多任务诞生以前就诞生了,这些老爷级的函数很多都没有考虑过多线程安全性,还有就是C++的全局对象静态对象等都需要调用它们的构造函数来初始化,而调用的主体就是线程,基于这些原因就要使用C/C++封装过的创建线程函数来创建线程,而CreateThread始终是Windows系统的API而已,它是不会考虑每种语言环境的特殊细节的,它只考虑系统的环境。

好了让我们继续打造线程的话题,要创建线程,实际核心就是准备一个线程函数,原型如下:

1、使用CreateThread时:

DWORD WINAPI ThreadProc(LPVOID lpParameter);

2、使用_beginthread时:

void __cdecl ThreadProc( void * pParameter );

3、使用_beginthreadex时:

unsigned int __stdcall ThreadProc(void* pParam);

其实上面三个函数原型都是很简单的,定义一个线程函数并不是什么难事,而真正困难的是对线程的理解和定义一个好的线程函数。这里我就不在多去谈论关于线程原理和如何写好一个线程函数的内容了,大家可以去参阅相关的文献。

现在我们接着讨论IOCP的专用线程如何编写,IOCP专用线程编写的核心工作就是调用一个同步函数GetQueuedCompletionStatus,为了理解的方便性,你可以想象这个函数的工作原理与那个有名的GetMessage是类似的,虽然这种比喻可能不太确切,但是他们工作方式是有些类似的地方,它们都会使调用它们的线程进入一种等待状态,只是这个函数不是等待消息队列中的消息,它是用来等待“被排队的完成状态”(就是它名字的含义)的,排队的完成状态,其实就是IO操作完成的通知(别告诉我你还不知道什么是IO操作),如果当前没有IO完成的通知,那么这个函数就会让线程进入“等待状态”,实际也就是一种“可警告”的状态,这样系统线程调度模块就会登记这个线程,一旦有IO完成通知,系统就会“激活”这个线程,立即分配时间片,让该线程开始继续执行,已完成IO完成通知的相关操作。

首先让我看看GetQueuedCompletionStatus的函数原型:

BOOL WINAPI GetQueuedCompletionStatus(

__in          HANDLE CompletionPort,

__out         LPDWORD lpNumberOfBytes,

__out         PULONG_PTR lpCompletionKey,

__out          LPOVERLAPPED* lpOverlapped,

__in          DWORD dwMilliseconds

);

第一个参数就是我们之前创建的那个完成端口内核对象的句柄,这个参数实际也就是告诉系统,我们当前的线程是归哪个完成端口对象来调度。

第二个参数是一个比较有用的参数,在函数返回后它将告诉我们这一次的IO操作实际传输或者接收了多少个字节的信息,这对于我们校验数据收发完整性非常有用。

第三个参数是与完成端口句柄绑定的一个一对一的数据指针,当然这个数据是我们绑到这个完成端口句柄上的,其实这个参数也是类似本人博客文章中所提到的那个“火车头”的作用的,它的作用和意义就是在我们得到完成通知时,可以拿到我们在最开初创建完成端口对象时绑定到句柄上的一个自定义的数据。这里给一个提示就是,在用C++的类封装中,通常这个参数我们会在绑定时传递类的this指针,而在GetQueuedCompletionStatus返回时又可以拿到这个类的this指针,从而可以在这个完成线程中调用类的方法。

第四个参数就是在本人其它IOCP相关博文中详细介绍过的重叠操作的数据结构,它也是一个火车头,这里就不在赘述它的用法了,请大家查阅本人其它博文拙作。

第五个参数是一个等待的毫秒数,也就是GetQueuedCompletionStatus函数等待IO完成通知的一个最大时间长度,如果超过这个时间值,GetQueuedCompletionStatus就会返回,并且返回值一个0值,此时调用GetLastError函数会得到一个明确的WAIT_TIMEOUT,也就是说它等待超时了,也没有等到一个IO完成通知。这时我们可以做一些相应的处理,而最常见的就是再次调用GetQueuedCompletionStatus函数让线程进入IO完成通知的等待状态。当然我们可以传递一个INFINITE值,表示让此函数一直等待,直到有一个完成通知进入完成状态队列。当然也可以为这个参数传递0值,表示该函数不必等待,直接返回,此时他的工作方式有些类似PeekMessage函数。

函数的参数和原型都搞清楚了,下面就让我们来看看调用的例子:

UINT CALLBACK IOCPThread(void* pParam)

{

CoInitialize(NULL);

       DWORD dwBytesTrans = 0;

       DWORD dwPerData = 0;

LPOVERLAPPED lpOverlapped = NULL;

       while(1)

       {

              BOOL bRet = GetQueuedCompletionStatus( hICP,&dwBytesTrans

,&dwPerData,&lpOverlapped,INFINITE);

              if( NULL == lpOverlapped )

              {

                     DWORD dwError = GetLastError();

                     ......//错误处理

}

              PMYOVERLAPPED pMyOL

= CONTAINING_RECORD(lpOverlapped, MYOVERLAPPED, m_ol);

              if( !HasOverlappedIoCompleted(lpOverlapped) )

              {//检测到不是一个真正完成的状态

                     DWORD dwError = GetLastError();

                     ......//错误处理

              }

                     ...... //继续处理

}

       return 0;

}

在这个线程函数中,我们写了一个死循环,这个是必要的,因为这个线程要反复处理IO完成通知的操作。跟我们常见的消息循环是异曲同工。

有了线程函数,接着就是创建线程了,对于IOCP来说,创建多少线程其实是一个决策问题,一般的原则就是创建的实际线程数量,不应小于调用CreateIoCompletionPort创建完成端口对象时指定的那个最大并发线程数。一般的指导原则是:如果完成线程的任务比较繁重大多数情况下执行的是其它的慢速等待性质的操作(比如磁盘磁带读写操作,数据库查询操作,屏幕显示等)时,由于这些操作的特点,我们可以适当的提高初始创建的线程数量。但是如果是执行计算密集型的操作时(比如网游服务端的场景变换运算,科学计算,工程运算等等),就不易再靠增加线程数来提高性能,因为这类运算会比较耗费CPU,没法切换出当前CPU时间片,多余的线程反倒会造成因为频繁的线程切换而造成整个程序响应性能的下降,此时为了保证IOCP的响应性,可以考虑再建立线程池来接力数据专门进行计算,这也是我的博文《IOCP编程之“双节棍”》篇中介绍的用线程池接力进行计算并提高性能的思想的核心。

下面的例子展示了如何创建IOCP线程池中的线程:

SYSTEM_INFO si = {};

GetSystemInfo(&si);

//创建CPU个数个IOCP线程

for( int i = 0; i < si.dwNumberOfProcessors; i ++ )

{

UINT nThreadID = 0;

//以暂停的状态创建线程状态

HANDLE hThread = (HANDLE)_beginthreadex(NULL,0,IOCPThread

,(void*)pThreadData,CREATE_SUSPENDED,(UINT*)&nThreadID);

//然后判断创建是否成功

if( NULL == reinterpret_cast<UINT>(m_hThread)

                     || 0xFFFFFFFF == reinterpret_cast<UINT>(m_hThread) )

{//创建线程失败

              ......//错误处理

}

::ResumeThread(hThread);//启动线程

}

创建好了IOCP的线程池,就可以往IOCP线程池中添加用来等待完成的那些重叠IO操作的句柄了,比如:重叠IO方式的文件句柄,重叠IO操作方式的SOCKET句柄,重叠IO操作的命名(匿名)管道等等。上面的这个操作可以被称作将句柄绑定到IOCP,绑定的方法就是再次调用CreateIoCompletionPort函数,这次调用时,就需要明确的指定前两个参数了,例子如下:

//创建一个重叠IO方式的SOCKET

SOCKET skSocket = ::WSASocket(AF_INET,SOCK_STREAM,IPPROTO_IP,

                            NULL,0,WSA_FLAG_OVERLAPPED);

......//其它操作

//绑定到IOCP

CreateIoCompletionPort((HANDLE)skSocket,hICP,NULL,0);

由代码就可以看出这步操作就非常的简单了,直接再次调用CreateIoCompletionPort函数即可,只是这次调用的意义就不是创建一个完成端口对象了,而是将一个重叠IO方式的对象句柄绑定到已创建好的完成端口对象上。
   

至此整个IOCP的基础知识算是介绍完了,作为总结,可以回顾下几个关键步骤:

1、 用CreateIoCompletionPort创建完成端口;

2、 定义IOCP线程池函数,类似消息循环那样写一个“死循环”调用GetQueuedCompletionStatus函数,并编写处理代码;

3、 创建线程;

4、 将重叠IO方式的对象句柄绑定到IOCP上。

只要记住了上面4个关键步骤,那么使用IOCP就基本掌握了。最后作为补充,让我再来讨论下这个核心步骤之外的一些附带的步骤。

现在假设我们已经创建了一个这样的IOCP线程池,而且这个线程池也工作的非常好了,那么我们该如何与这个线程池中的线程进行交互呢?还有就是我们如何让这个线程池停下来?

其实这个问题可以很简单的来思考,既然IOCP线程池核心的线程函数中有一个类似消息循环的结构,那么是不是也有一个类似PostMessage之类的函数来向其发送消息,从而实现与IOCP线程的交互呢?答案是肯定的,这个函数就是PostQueuedCompletionStatus,现在看到它的名字,你应该已经猜到它的用途了吧?对了,它就是用来向这个类似消息循环的循环中发送自定义的“消息”的,当然,它不是真正的消息,而是一个模拟的“完成状态”。这个函数的原型如下:

BOOL WINAPI PostQueuedCompletionStatus(

__in          HANDLE CompletionPort,

__in          DWORD dwNumberOfBytesTransferred,

__in          ULONG_PTR dwCompletionKey,

__in          LPOVERLAPPED lpOverlapped

);

它的参数与GetQueuedCompletionStatus类似,其实为了理解上的简单,我们可以认为PostQueuedCompletionStatus的参数就是原样的被copy到了GetQueuedCompletionStatus,怎么调用这个函数就应该可以理解了。通常在需要停止整个IOCP线程池工作时,就可以调用这个函数发送一个特殊的标志,比如设定dwCompletionKey为NULL,并且在自定义lpOverlapped指针结构之后带上一个表示关闭的标志等。这样在线程函数中就可以通过判定这些条件而明确的知道当前线程池需要关闭。当然也可以定义其它的操作扩展码来指定IOCP线程池执行指定的操作。下面的例子代码演示了如何发送一个IO完成状态:

MYOVERLAPPED *pOL = new MYOVERLAPPED ;

.......//其它初始化代码

pOL->m_iOpCode = OP_CLOSE;//指定关闭操作码

.......

PostQueuedCompletionStatus(hICP,0,NULL,(LPOVERLAPPED)pOL);

至此IOCP的基础性的支持算是介绍完了,本篇文章的主要目的是为了让大家理解IOCP的本质和工作原理,为轻松驾驭IOCP这个编程模型打下坚实的基础。最终需要掌握的就是认识到IOCP其实就是一个管理IO操作的自定义线程池这一本质。实际编码时决策性的问题就是理解最大并发数和预创建线程数的意义,并根据实际情况设定一个合理的值。

posted @ 2010-06-24 20:25 小火球| 编辑 收藏

理论上,本文适用于boost的各个版本,尤其是最新版本1.39.0;适用于各种C++编译器,如VC6.0,VS2003,VS2005,VS2008,gcc,C++ Builder等。
一、下载
首先从boost官方主页http://www.boost.org下 载最新版boost安装包(目前最新版是1.39.0)。因为boost一部分类是需要编译成库才能使用的,所以我们还需要准备好boost专用的编译辅 助工具bjam。网上很多人都提倡直接使用boost安装包中附带的bjam源码来编译出bjam,但是之前需要修改若干配置脚本才能编译成功。个人认为 真没什么必要,费这劲毫无意义。boost官方网站在提供boost安装包下载链接的同时也提供与该版本安装包对应的bjam的下载,只有200多KB, 可以一同下载下来。
二、安装
将boost安装包解压至本地目录,如:E:\SDK\boost_1_39_0,然后将bjam.exe拷贝到该目录下(bjam必须与boost-build.jam在同级目录)。
三、编译
接下来就是最重要的编译步骤了。需要打开命令提示符(cmd.exe)窗口并执行bjam,可以使用--help参数来查看命令帮助。这里详细讲解一下 bjam的命令行参数,因为它非常重要。首先,它涉及到编程环境的搭建,你需要根据自己今后具体的使用环境来选择合适的命令行参数;其次,它影响到你的硬 盘空间,完全编译的话据说在3G以上,如果你同时拥有2个以上的IDE(如VC6和VC9共存)而且都要用到boost,那么占用多少硬盘就自己算吧…… 虽说如今大家的硬盘空间都不成问题,但就像本人一样崇尚合理利用资源不习惯铺张浪费提倡节俭的童子应该大有人在。综合以上两点因素,本人使用的bjam命 令如下:


bjam stage --toolset=msvc-9.0 --without-python --stagedir="E:\SDK\boost_1_39_0\vc9" link=shared runtime-link=shared threading=multi debug release
下面详细解释一下每个参数的含义,请务必仔细看完:
stage/install:stage表示只生成库(dll和lib),install还会生成包含头文件的include目录。本人推荐使用 stage,因为install生成的这个include目录实际就是boost安装包解压缩后的boost目录(E:\SDK \boost_1_39_0\boost,只比include目录多几个非hpp文件,都很小),所以可以直接使用,而且不同的IDE都可以使用同一套头 文件,这样既节省编译时间,也节省硬盘空间。


toolset:指定编译器,可选的如borland、gcc、msvc(VC6)、msvc-9.0(VS2008)等。


without/with:选择不编译/编译哪些库。本人不需要编译python库,所以排除之,可以根据各人需要选择,默认是全部编译。但是需要注意,如果选择编译python的话,是需要python语言支持的,应该到python官方主页http://www.python.org下载安装。


stagedir/prefix:stage时使用stagedir,install时使用prefix,表示编译生成文件的路径。推荐给不同的IDE指 定不同的目录,如VS2008对应的是E:\SDK\boost_1_39_0\vc9\lib,VC6对应的是E:\SDK\boost_1_39_0\vc6\lib,否则都生成到一个目录下面,难以管理。如果使用了install参数,那么还将生成头文件目录,vc9对应的就是E:\SDK\boost_1_39_0\vc9\include\boost-1_39\boost,vc6类似(光这路径都这样累赘,还是使用stage好)。


build-dir:编译生成的中间文件的路径。这个本人这里没用到,默认就在根目录(E:\SDK\boost_1_39_0)下,目录名为bin.v2,等编译完成后可将这个目录全部删除(没用了),所以不需要去设置。


link:生成动态链接库/静态链接库。生成动态链接库需使用shared方式,生成静态链接库需使用static方式。这里需要注意的是,static 方式下,最终生成的很多静态链接库大小都在几兆、几十兆,甚至接近百兆。这么大的库我们一般是不会采用静态链接方式的,所以这些库不推荐以static方式编译(without掉);如果已经编译了赶快删,肯定没用,否则将占用近1G的硬盘空间。以下是巨型库黑名单:wave、graph、math、 regex、test、program_options、serialization、signals。


runtime-link:动态/静态链接C/C++运行时库。同样有shared和static两种方式,这样runtime-link和link一共 可以产生4种组合方式。虽然它和link属性没有直接关系,但我们习惯上,一个工程如果用动态链接那么所有库都用动态链接,如果用静态链接那么所有库都用 静态链接。所以这样其实只需要编译2种组合即可,即link=shared runtime-link=shared和link=static runtime-link=static。


threading:单/多线程编译。一般都写多线程程序,当然要指定multi方式了;如果需要编写单线程程序,那么还需要编译单线程库,可以使用single方式。


debug/release:编译debug/release版本。一般都是程序的debug版本对应库的debug版本,所以两个都编译。


本人按以上方式分别编译了静态链接和动态链接两个版本后,整个E:\SDK\boost_1_39_0目录(包括安装包解压缩文件和编译生成的库文件)只 有250MB。事实上编译完成后安装包解压缩文件除了boost目录之外其他目录和文件已经可以删除了,这样还可以腾出150MB的空间来。不过我又研究 了一下,其实libs这个目录也很有用,它提供了所有Boost类的使用范例,平时可以作为参考;另外doc目录是一个完整的boost使用帮助文档,当 然最好也不要删了。这样剩下的几个目录和文件加起来也就十多兆,索性都给它们留一条生路吧。


呵呵,一个完整而又完美的boost目录就此诞生了。


如果图省事,不想了解这么多,那么有简单的方法,可以使用命令:


bjam --toolset=msvc-9.0 --build-type=complete


直接指定编译器以完全模式编译即可,这样可以满足今后的一切使用场合,但同时带来的后果是:
1、占用3G以上的硬盘空间
2、占用若干小时的编译时间
3、头文件和库文件存放于C:\Boost(个人非常反感)  所以可以写成bjam --toolset=msvc-9.0 --stagedir="D:\SDK\boost_1_39_0\vc9" --build-type=complete

4、生成的很多文件可以永远也用不上


四、配置
include目录:E:\SDK\boost_1_39_0
library目录:E:\SDK\boost_1_39_0\vc9\lib
添加到IDE相应的路径下面即可。

posted @ 2010-06-21 09:55 小火球 阅读(691) | 评论 (0)编辑 收藏

本文简单介绍了当前Windows支持的各种Socket I/O模型,如果你发现其中存在什么错误请务必赐教。

    一:select模型
    二:WSAAsyncSelect模型
    三:WSAEventSelect模型
    四:Overlapped I/O 事件通知模型
    五:Overlapped I/O 完成例程模型
    六:IOCP模型

    老陈有一个在外地工作的女儿,不能经常回来,老陈和她通过信件联系。他们的信会被邮递员投递到他们的信箱里。
    这和Socket模型非常类似。下面我就以老陈接收信件为例讲解Socket I/O模型~~~

一:select模型

老陈非常想看到女儿的信。以至于他每隔10分钟就下楼检查信箱,看是否有女儿的信~~~~~
在这种情况下,“下楼检查信箱”然后回到楼上耽误了老陈太多的时间,以至于老陈无法做其他工作。
select模型和老陈的这种情况非常相似:周而复始地去检查......如果有数据......接收/发送.......

使用线程来select应该是通用的做法:
procedure TListenThread.Execute;
var
  addr     : TSockAddrIn;
  fd_read  : TFDSet;
  timeout  : TTimeVal;
  ASock,
  MainSock : TSocket;
  len, i   : Integer;
begin
  MainSock := socket( AF_INET, SOCK_STREAM, IPPROTO_TCP );
  addr.sin_family := AF_INET;
  addr.sin_port := htons(5678);
  addr.sin_addr.S_addr := htonl(INADDR_ANY);
  bind( MainSock, @addr, sizeof(addr) );
  listen( MainSock, 5 );

  while (not Terminated) do
  begin
    FD_ZERO( fd_read );
    FD_SET( MainSock, fd_read );
    timeout.tv_sec  := 0;
    timeout.tv_usec := 500;
    if select( 0, @fd_read, nil, nil, @timeout ) > 0 then //至少有1个等待Accept的connection
    begin
      if FD_ISSET( MainSock, fd_read ) then
      begin
        for i:=0 to fd_read.fd_count-1 do //注意,fd_count <= 64,也就是说select只能同时管理最多64个连接
        begin
          len := sizeof(addr);
          ASock := accept( MainSock, addr, len );
          if ASock <> INVALID_SOCKET then
             ....//为ASock创建一个新的线程,在新的线程中再不停地select
        end;
      end;
    end;
  end; //while (not self.Terminated)

  shutdown( MainSock, SD_BOTH );
  closesocket( MainSock );
end;

二:WSAAsyncSelect模型

后来,老陈使用了微软公司的新式信箱。这种信箱非常先进,一旦信箱里有新的信件,盖茨就会给老陈打电话:喂,大爷,你有新的信件了!从此,老陈再也不必频繁上下楼检查信箱了,牙也不疼了,你瞅准了,蓝天......不是,微软~~~~~~~~
微软提供的WSAAsyncSelect模型就是这个意思。

WSAAsyncSelect模型是Windows下最简单易用的一种Socket I/O模型。使用这种模型时,Windows会把网络事件以消息的形势通知应用程序。
首先定义一个消息标示常量:
const WM_SOCKET = WM_USER + 55;
再在主Form的private域添加一个处理此消息的函数声明:
private
  procedure WMSocket(var Msg: TMessage); message WM_SOCKET;
然后就可以使用WSAAsyncSelect了:
var
  addr  : TSockAddr;
  sock  : TSocket;

  sock := socket( AF_INET, SOCK_STREAM, IPPROTO_TCP );
  addr.sin_family := AF_INET;
  addr.sin_port := htons(5678);
  addr.sin_addr.S_addr := htonl(INADDR_ANY);
  bind( m_sock, @addr, sizeof(SOCKADDR) );

  WSAAsyncSelect( m_sock, Handle, WM_SOCKET, FD_ACCEPT or FD_CLOSE );

  listen( m_sock, 5 );
  ....

应用程序可以对收到WM_SOCKET消息进行分析,判断是哪一个socket产生了网络事件以及事件类型:

procedure TfmMain.WMSocket(var Msg: TMessage);
var
  sock    : TSocket;
  addr    : TSockAddrIn;
  addrlen : Integer;
  buf     : Array [0..4095] of Char;
begin
  //Msg的WParam是产生了网络事件的socket句柄,LParam则包含了事件类型
  case WSAGetSelectEvent( Msg.LParam ) of
    FD_ACCEPT :
    begin
      addrlen := sizeof(addr);
      sock := accept( Msg.WParam, addr, addrlen );
      if sock <> INVALID_SOCKET then
         WSAAsyncSelect( sock, Handle, WM_SOCKET, FD_READ or FD_WRITE or FD_CLOSE );
    end;

    FD_CLOSE : closesocket( Msg.WParam );
    FD_READ  : recv( Msg.WParam, buf[0], 4096, 0 );
    FD_WRITE : ;
  end; 
end;

三:WSAEventSelect模型

后来,微软的信箱非常畅销,购买微软信箱的人以百万计数......以至于盖茨每天24小时给客户打电话,累得腰酸背痛,喝蚁力神都不好使~~~~~~
微软改进了他们的信箱:在客户的家中添加一个附加装置,这个装置会监视客户的信箱,每当新的信件来临,此装置会发出“新信件到达”声,提醒老陈去收信。盖茨终于可以睡觉了。

同样要使用线程:
procedure TListenThread.Execute;
var
  hEvent : WSAEvent;
  ret    : Integer;
  ne     : TWSANetworkEvents;
  sock   : TSocket;
  adr    : TSockAddrIn;
  sMsg   : String;
  Index,
  EventTotal : DWORD;
  EventArray : Array [0..WSA_MAXIMUM_WAIT_EVENTS-1] of WSAEVENT;
begin
  ...socket...bind...
  hEvent := WSACreateEvent();
  WSAEventSelect( ListenSock, hEvent, FD_ACCEPT or FD_CLOSE );
  ...listen...

  while ( not Terminated ) do
  begin
    Index := WSAWaitForMultipleEvents( EventTotal, @EventArray[0], FALSE, WSA_INFINITE, FALSE );
    FillChar( ne, sizeof(ne), 0 );
    WSAEnumNetworkEvents( SockArray[Index-WSA_WAIT_EVENT_0],  EventArray[Index-WSA_WAIT_EVENT_0], @ne );

    if ( ne.lNetworkEvents and FD_ACCEPT ) > 0 then
    begin
      if ne.iErrorCode[FD_ACCEPT_BIT] <> 0 then
         continue;

      ret := sizeof(adr);
      sock := accept( SockArray[Index-WSA_WAIT_EVENT_0], adr, ret );
      if EventTotal > WSA_MAXIMUM_WAIT_EVENTS-1 then//这里WSA_MAXIMUM_WAIT_EVENTS同样是64
      begin
        closesocket( sock );
        continue;
      end;

      hEvent := WSACreateEvent();
      WSAEventSelect( sock, hEvent, FD_READ or FD_WRITE or FD_CLOSE );
      SockArray[EventTotal] := sock;
      EventArray[EventTotal] := hEvent;
      Inc( EventTotal );
    end;

    if ( ne.lNetworkEvents and FD_READ ) > 0 then
    begin
      if ne.iErrorCode[FD_READ_BIT] <> 0 then
         continue;
      FillChar( RecvBuf[0], PACK_SIZE_RECEIVE, 0 );
      ret := recv( SockArray[Index-WSA_WAIT_EVENT_0], RecvBuf[0], PACK_SIZE_RECEIVE, 0 );
      ......
    end;
  end;
end;


四:Overlapped I/O 事件通知模型

后来,微软通过调查发现,老陈不喜欢上下楼收发信件,因为上下楼其实很浪费时间。于是微软再次改进他们的信箱。新式的信箱采用了更为先进的技术,只要用户告诉微软自己的家在几楼几号,新式信箱会把信件直接传送到用户的家中,然后告诉用户,你的信件已经放到你的家中了!老陈很高兴,因为他不必再亲自收发信件了!

Overlapped I/O 事件通知模型和WSAEventSelect模型在实现上非常相似,主要区别在“Overlapped”,Overlapped模型是让应用程序使用重叠数据结构(WSAOVERLAPPED),一次投递一个或多个Winsock I/O请求。这些提交的请求完成后,应用程序会收到通知。什么意思呢?就是说,如果你想从socket上接收数据,只需要告诉系统,由系统为你接收数据,而你需要做的只是为系统提供一个缓冲区~~~~~
Listen线程和WSAEventSelect模型一模一样,Recv/Send线程则完全不同:
procedure TOverlapThread.Execute;
var
  dwTemp : DWORD;
  ret    : Integer;
  Index  : DWORD;
begin
  ......

  while ( not Terminated ) do
  begin
    Index := WSAWaitForMultipleEvents( FLinks.Count, @FLinks.Events[0], FALSE, RECV_TIME_OUT, FALSE );
    Dec( Index, WSA_WAIT_EVENT_0 );
    if Index > WSA_MAXIMUM_WAIT_EVENTS-1 then //超时或者其他错误
       continue;

    WSAResetEvent( FLinks.Events[Index] );
    WSAGetOverlappedResult( FLinks.Sockets[Index], FLinks.pOverlaps[Index], @dwTemp, FALSE, FLinks.pdwFlags[Index]^ );

    if dwTemp = 0 then //连接已经关闭
    begin
      ......
      continue;
    end else
    begin
      fmMain.ListBox1.Items.Add( FLinks.pBufs[Index]^.buf );
    end;

    //初始化缓冲区
    FLinks.pdwFlags[Index]^ := 0;
    FillChar( FLinks.pOverlaps[Index]^, sizeof(WSAOVERLAPPED), 0 );
    FLinks.pOverlaps[Index]^.hEvent := FLinks.Events[Index];
    FillChar( FLinks.pBufs[Index]^.buf^, BUFFER_SIZE, 0 );

    //递一个接收数据请求
    WSARecv( FLinks.Sockets[Index], FLinks.pBufs[Index], 1, FLinks.pdwRecvd[Index]^, FLinks.pdwFlags[Index]^, FLinks.pOverlaps[Index], nil );
  end;
end;

五:Overlapped I/O 完成例程模型

老陈接收到新的信件后,一般的程序是:打开信封----掏出信纸----阅读信件----回复信件......为了进一步减轻用户负担,微软又开发了一种新的技术:用户只要告诉微软对信件的操作步骤,微软信箱将按照这些步骤去处理信件,不再需要用户亲自拆信/阅读/回复了!老陈终于过上了小资生活!

Overlapped I/O 完成例程要求用户提供一个回调函数,发生新的网络事件的时候系统将执行这个函数:
procedure WorkerRoutine( const dwError, cbTransferred : DWORD; const
          lpOverlapped : LPWSAOVERLAPPED; const dwFlags : DWORD ); stdcall;
然后告诉系统用WorkerRoutine函数处理接收到的数据:
WSARecv( m_socket, @FBuf, 1, dwTemp, dwFlag, @m_overlap, WorkerRoutine );
然后......没有什么然后了,系统什么都给你做了!微软真实体贴!
while ( not Terminated ) do//这就是一个Recv/Send线程要做的事情......什么都不用做啊!!!
begin
  if SleepEx( RECV_TIME_OUT, True ) = WAIT_IO_COMPLETION then //
  begin
    ;
  end else
  begin
    continue;
  end;
end;

六:IOCP模型

微软信箱似乎很完美,老陈也很满意。但是在一些大公司情况却完全不同!这些大公司有数以万计的信箱,每秒钟都有数以百计的信件需要处理,以至于微软信箱经常因超负荷运转而崩溃!需要重新启动!微软不得不使出杀手锏......
微软给每个大公司派了一名名叫“Completion Port”的超级机器人,让这个机器人去处理那些信件!

“Windows NT小组注意到这些应用程序的性能没有预料的那么高。特别的,处理很多同时的客户请求意味着很多线程并发地运行在系统中。因为所有这些线程都是可运行的[没有被挂起和等待发生什么事],Microsoft意识到NT内核花费了太多的时间来转换运行线程的上下文[Context],线程就没有得到很多CPU时间来做它们的工作。大家可能也都感觉到并行模型的瓶颈在于它为每一个客户请求都创建了一个新线程。创建线程比起创建进程开销要小,但也远不是没有开销的。我们不妨设想一下:如果事先开好N个线程,让它们在那hold[堵塞],然后可以将所有用户的请求都投递到一个消息队列中去。然后那N个线程逐一从消息队列中去取出消息并加以处理。就可以避免针对每一个用户请求都开线程。不仅减少了线程的资源,也提高了线程的利用率。理论上很不错,你想我等泛泛之辈都能想出来的问题,Microsoft又怎会没有考虑到呢?”-----摘自nonocast的《理解I/O Completion Port》

先看一下IOCP模型的实现:

//创建一个完成端口
FCompletPort := CreateIoCompletionPort( INVALID_HANDLE_VALUE, 0,0,0 );

//接受远程连接,并把这个连接的socket句柄绑定到刚才创建的IOCP上
AConnect := accept( FListenSock, addr, len);
CreateIoCompletionPort( AConnect, FCompletPort, nil, 0 );

//创建CPU数*2 + 2个线程
for i:=1 to si.dwNumberOfProcessors*2+2 do
begin
  AThread := TRecvSendThread.Create( false );
  AThread.CompletPort := FCompletPort;//告诉这个线程,你要去这个IOCP去访问数据
end;

OK,就这么简单,我们要做的就是建立一个IOCP,把远程连接的socket句柄绑定到刚才创建的IOCP上,最后创建n个线程,并告诉这n个线程到这个IOCP上去访问数据就可以了。

再看一下TRecvSendThread线程都干些什么:

procedure TRecvSendThread.Execute;
var
  ......
begin
  while (not self.Terminated) do
  begin
    //查询IOCP状态(数据读写操作是否完成)
    GetQueuedCompletionStatus( CompletPort, BytesTransd, CompletKey, POVERLAPPED(pPerIoDat), TIME_OUT );

    if BytesTransd <> 0  then
       ....;//数据读写操作完成

    //再投递一个读数据请求
    WSARecv( CompletKey, @(pPerIoDat^.BufData), 1, BytesRecv, Flags, @(pPerIoDat^.Overlap), nil );
  end;
end;

读写线程只是简单地检查IOCP是否完成了我们投递的读写操作,如果完成了则再投递一个新的读写请求。
应该注意到,我们创建的所有TRecvSendThread都在访问同一个IOCP(因为我们只创建了一个IOCP),并且我们没有使用临界区!难道不会产生冲突吗?不用考虑同步问题吗?
呵呵,这正是IOCP的奥妙所在。IOCP不是一个普通的对象,不需要考虑线程安全问题。它会自动调配访问它的线程:如果某个socket上有一个线程A正在访问,那么线程B的访问请求会被分配到另外一个socket。这一切都是由系统自动调配的,我们无需过问。

posted @ 2010-05-24 13:47 小火球 阅读(223) | 评论 (0)编辑 收藏

仅列出标题
共4页: 1 2 3 4 

posts - 28, comments - 3, trackbacks - 0, articles - 0

Copyright © 小火球