zoukankan      html  css  js  c++  java
  • 完成端口之一

    一、简介

      ”完成端口“模型是迄今为止最复杂的一种I/O模型,但是,若一个应用程序要同时需要管理很多套接字,那么采用这种模型,往往可以达到最佳系统性能,随着系统内安装的CPU数量增加,应用程序的性能也可能线性提升。

      大家可以这样理解,一个完成端口其实就是一个完成I/O的通知队列,由操作系统把已完成的重叠I/O请求通知放入这个队列中,当某项I/O操作一旦完成,某个可以对该操作结果处理的工作线程就会收到通知,工作者线程再去做一些其他的善后工作,

      通常情况下我们会在应用程序中创建一定数量的工作线程来处理这些通知,线程数量取决于应用程序的特定需求,理想情况下,线程数量等于处理器数量,不过这叶要求任何线程都不应该执行诸如同步读写、等待事件通知等阻塞模型的操作,以免线程阻塞每个线程都将分到一定的CPU事件,在其期间该线程可以运行,然后另一个线程将分到一个时间片开始执行,如果某个线程执行了阻塞型的操作,操作系统将夺其未使用的剩余时间片,也就是说,前一个线程没有充分使用其时间片,当发生这样的情况时,应用程序应该准备其他线程来充分利用这些时间片。

    二、使用详细说明

    1、使用这种模型之前,首先要创建一个I/O完成端口对象,用它面向任意数量的套接字句柄,管理多个I/O请求,要做到这一点,需要调用CreateIoCompletionPort函数

    HANDLE
    WINAPI
    CreateIoCompletionPort(
        _In_ HANDLE FileHandle,
        _In_opt_ HANDLE ExistingCompletionPort,
        _In_ ULONG_PTR CompletionKey,
        _In_ DWORD NumberOfConcurrentThreads
        );
    View Code

    该函数有2个功能,(1)用于创建一个完成端口对象,(2)将一个句柄同完成对象关联到一起。

    如果仅仅为了创建一个完成端口对象,唯一注意的参数便是NumberOfConcurrentThreads(并发线程的数量),前三个参数可以忽略,

     NumberOfConcurrentThreads:它定义了一个完成端口上,同时允许执行的线程数量,理想情况下,我们希望每个处理器各自负责一个线程的运行,为完成端口提供服务,避免过于频繁线程上下文切换,若将该参数设为0,表面系统安装了多少个处理器,便允许同时运行多少个线程,可用下述代码创建一个I/O完成端口;

    HANDLE m_hCompletionPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);

    2.工作线程与完成端口

      成功创建一个完成端口后,便可以开始将套接字句柄与其关联到一起,但在关联套接字之前,首先必须创建一个或者多个“工作线程",以便在I/O请求投递给完成端口后,为完成端口提供服务,到底要创建多少个线程呢?在此,要记住一点,我们调用CreateIoCompletionPort时指定的并发线程数量,与打算创建的工作线程数量相比,他们代表的不是同一件事情,

      CreateIoCompletionPort函数的NumberOfConcurrentThreads参数明确提示系统:在一个完成端口上一个只允许n个工作线程运行,加入在完成端口上创建的工作线程数量超出n个,那么在同一时刻,最多只允许n个线程运行,在一段较短的时间内,系统可能超过这个值,但很快便会把它减少至事先在CreateIoCompletionPort函数中设定的值。

      那么,为何实际创建的工作线程数量有时要比CreateIoCompletionPort函数中设定的多一些呢?原因如下:

      这主要取决于应用程序的总体设计情况,假定我们的某个线程调用了一个函数,比如Sleep或者WaitForSingObject,进入了暂停(锁定或者挂起)状态,那么运行另一个线程代替它的位置,换言之,我们希望随时都能执行尽可能多的线程,

    3.一旦在完成端口上拥有足够多的工作线程来为I/O请求提供服务,便可着手将套接字句柄同完成端口关联到一起,这要求我们在一个现有的完成端口上,调用CreateIoCompletionPort函数,同时为前三个参数提供套接字的信息。

      FileHandle:指定一个要同完成端口关联的套接字句柄;

      ExistingCompletionPort:参数指定的是一个现有的完成端口;

      CompletionKey(完成键):参数指定某个套接字句柄关联在一起的”单句柄数据“,可将其作为指向一个数据结构的指针,再次数据结构中,同时包含了套接字的句柄,已经套接字有关的其他信息,如IP等,为完成端口提供服务的线程函数可通过这个参数,取得与套接字句柄有关的信息。

    4.工作线程要做的事情

      将套接字句柄与一个完成端口关联在一起后,使可投递发送与接收请求,开始I/O请求的处理。接下来,可开始依赖完成端口来接收有关I/O操作完成情况的通知。从本质上说,完成端口模型利用了Win32重叠I/O机制,在这种机制中,像WSASend和WSARecv这样的WinsockAPI调用会立即返回,此时,需要有我们的应用程序负责在以后的某个时间,通过一个OVERLAPPED结构,来接受之前调用请求的结果。

      在完成端口模型中,要做到这一点,需要使用GetQueuedCompletionStatus(获取排队完成状态)函数,让一个或多个工作线程在完成端口上等待I/O请求完成的通知,函数如下:

     

    BOOL
    WINAPI
    GetQueuedCompletionStatus(
        _In_ HANDLE CompletionPort,
        _Out_ LPDWORD lpNumberOfBytesTransferred,
        _Out_ PULONG_PTR lpCompletionKey,
        _Out_ LPOVERLAPPED * lpOverlapped,
        _In_ DWORD dwMilliseconds
        );
    CompletionPort:参数对应于要在上面等待的完成端口;
    lpNumberOfBytesTransferred:参数负责在完成一次I/O操作(如WSASend或WSARecv)后,接收时间传输的字节数;
    lpCompletionKey:参数为原先传递给CreateIoCompletionPort函数的第三个参数,“单句柄数据” ,如我们早先所述,大家最好将套接字句柄保存在这个键(Key)中:
    lpOverlapped:参数用于接收完成I/O操作的重叠结果,这实际是一个相当重要的参数,因为可用它获取每个I/O操作的数据:
    dwMilliseconds:参数用于指定希望等待一个完成数据包在完成端口上出现的时间,即超时时间,若设置为INFINITE,会一直等待下去:

    5、“单句柄数据”和 单 I/O 操作数据
    一个工作者线程从 GetQueuedCompletionStatus 函数接收到 I/O 完成通知后,在 lpCompletionKey 和 lpOverlapped 参数中,
    会包含一些重要的套接字信息。利用这些信息,可通过完成端口,继续在一个套接字上进行其他的处理。

    通过这些参数,可获得两方面重要的套接字数据:“单句柄数据”以及单 I/O 操作数据。

    其中,lpCompletionKey参数包含了“单句柄数据”,因为在一个套接字首次与完成端口关联到一起的时候,
    那些数据便与一个特定的套接字句柄对应起来了。这些数据正是我们在调用 CreateIoCompletionPort 函数时候,通过 CompletionKey 参数传递的。
    通常情况下,应用程序会将与 I/O 请求有关的套接字句柄及其他的一些相关信息保存在这里;

    lpOverlapped 参数则包含了一个 OVERLAPPED 结构,在它后面跟随“单 I/O 操作数据”。
    单 I/O 操作数据可以是追加到一个 OVERLAPPED 结构末尾的、任意数量的字节。
    假如一个函数要求用到一个 OVERLAPPED 结构,我们便必须将这样的一个结构传递进去,以满足它的要求。
    要想做到这一点,一个简单的方法是定义一个结构,然后将 OVERLAPPED 结构作为新结构的第一个元素使用。

    举个例子来说,可定义下述数据结构,实现对单 I/O 操作数据的管理:

    class COverLappedEX
    {
    public:
        OVERLAPPED m_OLap;
        IO_TYPE m_IOType;
        char m_szBuf[MAX_BUF_SIZE];
    
        COverLappedEX(IO_TYPE ioType)
        {
            ZeroMemory(&m_OLap, sizeof(OVERLAPPED));
            m_IOType = ioType;
            ZeroMemory(m_szBuf, MAX_BUF_SIZE);
        }
    
    private:
    
    };

      该结构演示了通常与 I/O 操作关联的一些重要的数据元素,比如刚才完成的那个 I/O 操作的类型(发送或接收请求),用 OperationType 字段表示,
    同时,用于已完成 I/O 操作数据的缓冲区 szBuffer 也是非常有用的。如果想调用一个 Winsock API 函数(如:WSASend、WSARecv),要为其分配一个 OVERLAPPED 结构,
    这时,就可以将我们的结构强制转换成一个 OVERLAPPED 指针,或者从结构中将 OVERLAPPED 元素的地址取出来

      在工作线程的后面部分,等 GetQueuedCompletionStatus 函数返回了一个重叠结构(和完成键)后,
    便可通过 OperationType 成员,看出到底是哪个操作投递到了这个句柄之上(只需将返回的重叠结强制转换为自己的 PER_IO_OPERATION_DATA 结构)。
    对单 I/O 操作数据来说,它最大的一个优点便是允许我们在同一个句柄上,同时管理多个 I/O 操作(读/写,多个读,多个写,等等)。

    6、正确地关闭 I/O 完成端口
    如何正确地关闭 I/O 完成端口,特别是同时运行了一个或多个线程,在几个不同的套接字上执行 I/O 操作的时候。
    要避免的一个重要问题是在进行重叠 I/O 操作的同时,强行释放一个 OVERLAPPED 结构。
    要想避免出现这种情况,最好的办法是针对每个套接字句柄,调用 closesocket 函数,任何尚未进行的重叠 I/O 操作都会完成。一旦所有套接字句柄都已关闭,
    便需在完成端口上,终止所有工作者线程的运行。要想做到这一点,需要使用 PostQueuedCompletionStatus 函数,向每个工作者线程都发送一个特殊的完成数据包。
    该函数会指示每个线程都“立即结束并退出”。下面是 PostQueuedCompletionStatus 函数的定义:
    BOOL WINAPI PostQueuedCompletionStatus(
    __in HANDLE CompletionPort,
    __in DWORD dwNumberOfBytesTransferred,
    __in ULONG_PTR dwCompletionKey,
    __in LPOVERLAPPED lpOverlapped
    );
    ● CompletionPort 参数指定想向其发送一个完成数据包的完成端口对象;
    ● 而就 dwNumberOfBytesTransferred、dwCompletionKey 和 lpOverlapped 三个参数来说,每一个都允许我们指定一个值,
    直接传递给 GetQueuedCompletionStatus 函数中对应的参数。这样一来,一个工作者线程收到传递过来的三个 GetQueuedCompletionStatus 函数参数后,
    便可根据由这三个参数的某一个设置的特殊值,决定何时或者应该怎样退出。
    例如,可用 dwCompletionPort 参数传递 0 值,而一个工作者线程会将其解释成中止指令。
    一旦所有工作者线程都已关闭,便可使用 CloseHandle 函数,关闭完成端口,最终安全退出程序。

    三、完成端口的具体实现步骤:

      1)创建一个完成端口,第4个参数保持为0,指定在完成端口上,每个处理器一次只允许执行一个工作线程;

      2)判断系统到底安装了多少个处理器;

      3)创建工作者线程,根据步骤2)得到的处理器信息,在完成端口上,为已完成的I/O请求提供服务;

      4)准备好一个监听套接字,进入监听。

      5)使用accept函数,接收进入的连接请求;

      6)创建一个数据结构,用于容纳”单句柄数据“,同时在结构中存入接受的套接字句柄,调用CreateIoCompletionPort函数,将从accept返回的新套接字句柄同完成端口关联到一起,通过完成键(CompletionKey)参数,将单句柄数据结构传递给CreateIoCompletionPort函数;

      7)调用CreateIoCompletionPort函数,将从accept返回的新套接字同完成端口关联到一起,通过完成键(CompletionKey)参数,将单句柄数据结构传递给CreateIoCompletionPort函数;

      8)开始在已接受的连接上进行I/O操作,在此,我们希望通过重叠I/O机制,在新建的套接字上投递一个或多个异步WSARecv或WSASend请求,这些I/O请求完成后,一个工作线程为I/O请求提供服务,同时继续处理未来的其他I/O请求;

      9)重复 5)~ 8),知道服务器终止

      

    111
  • 相关阅读:
    重载和重写的定义
    方法的重载与重写有什么区别?
    java: while 和do while区别
    java中的运算符
    java 8种基本数据类型
    java.面向对象特征
    java语言的特点
    java.注释类型
    char 和 varchar2 区别
    使用sql对数据库进行简单的增删改查
  • 原文地址:https://www.cnblogs.com/zwj-199306231519/p/13935424.html
Copyright © 2011-2022 走看看