完成IO使用总结IOCP

http://blog.csdn.net/xionghaoaizhangruyun/article/details/8184863



IOCPI/O Completion PortI/O完成端口)是性能最好的一种I/O模型。它是应用程序使用线程池处理异步I/O请求的一种机制。在处理多个并发的异步I/O请求时,以往的模型都是在接收请求是创建一个线程来应答请求。这样就有很多的线程并行地运行在系统中。而这些线程都是可运行的,Windows内核花费大量的时间在进行线程的上下文切换,并没有多少时间花在线程运行上。再加上创建新线程的开销比较大,所以造成了效率的低下。

 

调用的步骤如下:

抽象出一个完成端口大概的处理流程:

1:创建一个完成端口。

2:创建一个线程A

3A线程循环调用GetQueuedCompletionStatus()函数来得到IO操作结果,这个函数是个阻塞函数。

4:主线程循环里调用accept等待客户端连接上来。

5:主线程里accept返回新连接建立以后,把这个新的套接字句柄用CreateIoCompletionPort关联到完成端口,然后发出一个异步的WSASend或者WSARecv调用,因为是异步函数,WSASend/WSARecv会马上返回,实际的发送或者接收数据的操作由WINDOWS系统去做。

6:主线程继续下一次循环,阻塞在accept这里等待客户端连接。

7WINDOWS系统完成WSASend或者WSArecv的操作,把结果发到完成端口。

8A线程里的GetQueuedCompletionStatus()马上返回,并从完成端口取得刚完成的WSASend/WSARecv的结果。

9:在A线程里对这些数据进行处理(如果处理过程很耗时,需要新开线程处理),然后接着发出WSASend/WSARecv,并继续下一次循环阻塞在GetQueuedCompletionStatus()这里。

归根到底概括完成端口模型一句话:

我们不停地发出异步的WSASend/WSARecvIO操作,具体的IO处理过程由WINDOWS系统完成,WINDOWS系统完成实际的IO处理后,把结果送到完成端口上(如果有多个IO都完成了,那么就在完成端口那里排成一个队列)。我们在另外一个线程里从完成端口不断地取出IO操作结果,然后根据需要再发出WSASend/WSARecvIO操作。

 

IOCP模型是事先开好了N个线程,存储在线程池中,让他们hold。然后将所有用户的请求都投递到一个完成端口上,然后N个工作线程逐一地从完成端口中取得用户消息并加以处理。这样就避免了为每个用户开一个线程。既减少了线程资源,又提高了线程的利用率。

 

完成端口模型是怎样实现的呢?我们先创建一个完成端口(::CreateIoCompletioPort())。然后再创建一个或多个工作线程,并指定他们到这个完成端口上去读取数据。我们再将远程连接的套接字句柄关联到这个完成端口(还是用::CreateIoCompletionPort())。一切就OK了。

 

工作线程都干些什么呢?首先是调用::GetQueuedCompletionStatus()函数在关联到这个完成端口上的所有套接字上等待I/O的完成。再判断完成了什么类型的I/O。一般来说,有三种类型的I/OOP_ACCEPTOP_READOP_WIRTE。我们到数据缓冲区内读取数据后,再投递一个或是多个同类型的I/O即可(::AcceptEx()::WSARecv()::WSASend())。对读取到的数据,我们可以按照自己的需要来进行相应的处理。

 

为此,我们需要一个以OVERLAPPED(重叠I/O)结构为第一个字段的per-I/O数据自定义结构。

 

typedef struct _PER_IO_DATA

{

        OVERLAPPED ol;       //重叠I/O结构

        char buf[BUFFER_SIZE];   //数据缓冲区

        int nOperationType;         //I/O操作类型

#define OP_READ 1

#define OP_WRITE 2

#define OP_ACCEPT 3

} PER_IO_DATA, *PPER_IO_DATA;

 

将一个PER_IO_DATA结构强制转化成一个OVERLAPPED结构传给::GetQueuedCompletionStatus()函数,返回的这个PER_IO_DATA结构的的nOperationType就是I/O操作的类型。当然,这些类型都是在投递I/O请求时自己设置的。

 

这样一个IOCP服务器的框架就出来了。当然,要做一个好的IOCP服务器,还有考虑很多问题,如内存资源管理、接受连接的方法、恶意的客户连接、包的重排序等等。以上是个人对于IOCP模型的一些理解与看法,还有待完善。另外各Winsock API的用法参见MSDN

 

 

完成端口中的单句柄数据结构与单IO数据结构的理解与设计

 

完成端口模型,针对于win平台的其它异步网络模型而言,最大的好处,除了性能方面的卓越外,还在于完成端口在传递网络事件的通知时,可以一并传递与此事件相关的应用层数据。这个应用层数据,体现在两个方面:一是单句柄数据,二是单io数据。

 

  getqueuedcompletionstatus函数的原型如下:

  winbaseapi

  bool

  winapi

  getqueuedcompletionstatus(

      in handle completionport,

      out lpdwordlpnumberofbytestransferred,

      out pulong_ptr lpcompletionkey,

      out lpoverlapped *lpoverlapped,

      in dword dwmilliseconds

     );

  其中,我们把第三个参数lpcompletionkey称为完成键,由它传递的数据称为单句柄数据。我们把第四个参数lpoverlapped称为重叠结构体,由它传递的数据称为单io数据。

 

  以字面的意思来理解,lpcompletionkey内包容的东西应该是与各个socket一一对应的,而lpoverlapped是与每一次的wsarecvwsasend操作一一对应的。

 

  在网络模型的常见设计中,当一个客户端连接到服务器后,服务器会通过acceptacceptex创建一个socket,而应用层为了保存与此socket相关的其它信息(比如:该socket所对应的sockaddr_in结构体数据,该结构体内含客户端ip等信息,以及为便于客户端的逻辑包整理而准备的数据整理缓冲区等),往往需要创建一个与该socket一一对应的客户端底层通信对象,这个对象可以负责保存仅在网络层需要处理的数据成员和方法,然后我们需要将此客户端底层通信对象放入一个类似于listmap的容器中,待到需要使用的时候,使用容器的查找算法根据socket值找到它所对应的对象然后进行我们所需要的操作。

 

  让人非常高兴的是,完成端口“体贴入微”,它已经帮我们在每次的完成事件通知时,稍带着把该socket所对应的底层通信对象的指针送给了我们,这个指针就是lpcompletionkey。也就是说,当我们从getqueuedcompletionstatus函数取得一个数据接收完成的通知,需要将此次收到的数据放到该socket所对应的通信对象整理缓冲区内对数据进行整理时,我们已经不需要去执行listmap等的查找算法,而是可以直接定位这个对象了,当客户端连接量很大时,频繁查表还是很影响效率的。哇哦,太帅了,不是吗?呵呵。

 

  基于以上的认识,我们的lpcompletionkey对象可以设计如下:

  typedefstruct per_handle_data

  {

    socketsocket;             //本结构体对应的socket

    sockaddr_inaddr;          //用于存放客户端ip等信息

    chardatabuf[ 2*max_buffer_size ];  //整理缓冲区,用于存放每次整理时的数据

  }

 

  per_handle_datasocket的绑定,通过createiocompletionport完成,将该结构体地址作为该函数的第三个参数传入即可。而per_handle_data结构体中addr成员,是在accept执行成功后进行赋值的。databuf则可以在每次wsarecv操作完成,需要整理缓冲区数据时使用。

 

  下面我们再来看看完成端口的收、发操作中所使用到的重叠结构体overlapped

 

  关于重叠io的知识,请自行google相关资料。简单地说,overlapped是应用层与核心层交互共享的数据单元,如果要执行一个重叠io操作,必须带有overlapped结构。在完成端口中,它允许应用层对overlapped结构进行扩展和自定义,允许应用层根据自己的需要在overlapped的基础上形成新的扩展overlapped结构。一般地,扩展的overlapped结构中,要求放在第一个的数据成员是原overlapped结构。我们可以形如以下方式定义自己的扩展overlapped结构:

  typedefstruct per_io_data

  {

    overlappedovl;

    wsabuf           buf;

    char                    recvdatabuf[max_buffer_size ];   //接收缓冲区

    char                    senddatabuf[max_buffer_size ];   //发送缓冲区

    optype              optype;                                                      //操作类型:发送、接收或关闭等

  }

  

  在执行wsasendwsarecv操作时,应用层会将扩展overlapped结构的地址传给核心,核心完成相应的操作后,仍然通过原有的这个结构传递操作结果,比如“接收”操作完成后,recvdatabuf里存放便是此次接收下来的数据。

 

  根据各自应用的不同,不同的完成端口设计者可能会设计出不同的per_handle_data

per_io_data,我这里给出的设计也只是针对自己的应用场合的,不一定就适合你。但我想,最主要的还是要搞明白per_handle_dataper_io_data两种结构体的含义、用途,以及调用流程。

 

CRITICAL_SECTION理解的总结

 

很多人对CRITICAL_SECTION的理解是错误的,认为CRITICAL_SECTION是锁定了资源,其实,CRITICAL_SECTION是不能够“锁定”资源的,它能够完成的功能,是同步不同线程的代码段。简单说,当一个线程执行了EnterCritialSection之后,cs里面的信息便被修改了,以指明哪一个线程占用了它。而此时,并没有任何资源被“锁定”。不管什么资源,其它线程都还是可以访问的(当然,执行的结果可能是错误的)。只不过,在这个线程尚未执行LeaveCriticalSection之前,其它线程碰到EnterCritialSection语句的话,就会处于等待状态,相当于线程被挂起了。这种情况下,就起到了保护共享资源的作用。

     也正由于CRITICAL_SECTION是这样发挥作用的,所以,必须把每一个线程中访问共享资源的语句都放在EnterCritialSectionLeaveCriticalSection之间。这是初学者很容易忽略的地方。

当然,上面说的都是对于同一个CRITICAL_SECTION而言的。如果用到两个CRITICAL_SECTION,比如说:

第一个线程已经执行了EnterCriticalSection(&cs)并且还没有执行LeaveCriticalSection(&cs),这时另一个线程想要执行EnterCriticalSection(&cs2),这种情况是可以的(除非cs2已经被第三个线程抢先占用了)。这也就是多个CRITICAL_SECTION实现同步的思想。

 

      比如说我们定义了一个共享资源dwTime[100],两个线程ThreadFuncAThreadFuncB都对它进行读写操作。当我们想要保证dwTime[100]的操作完整性,即不希望写到一半的数据被另一个线程读取,那么用CRITICAL_SECTION来进行线程同步如下:

第一个线程函数:

DWORD WINAPI ThreadFuncA(LPVOID lp)

{

EnterCriticalSection(&cs);

...

// 操作dwTime

...

LeaveCriticalSection(&cs);

return 0;

}

写出这个函数之后,很多初学者都会错误地以为,此时csdwTime进行了锁定操作,dwTime处于cs的保护之中。一个“自然而然”的想法就是——csdwTime一一对应上了。这么想,就大错特错了。dwTime并没有和任何东西对应,它仍然是任何其它线程都可以访问的。

 

如果你像如下的方式来写第二个线程,那么就会有问题:

DWORD WINAPI ThreadFuncB(LPVOID lp)

{

...

// 操作dwTime

...

return 0;

}

当线程ThreadFuncA执行了EnterCriticalSection(&cs),并开始操作dwTime[100]的时候,线程ThreadFuncB可能随时醒过来,也开始操作dwTime[100],这样,dwTime[100]中的数据就被破坏了。

     为了让CRITICAL_SECTION发挥作用,我们必须在访问dwTime的任何一个地方都加上EnterCriticalSection(&cs)LeaveCriticalSection(&cs)语句。所以,必须按照下面的方式来写第二个线程函数:

DWORD WINAPI ThreadFuncB(LPVOID lp)

{

EnterCriticalSection(&cs);

...

// 操作dwTime

...

LeaveCriticalSection(&cs);

return 0;

}

      这样,当线程ThreadFuncB醒过来时,它遇到的第一个语句是EnterCriticalSection(&cs),这个语句将对cs变量进行访问。如果这个时候第一个线程仍然在操作dwTime[100]cs变量中包含的值将告诉第二个线程,已有其它线程占用了cs。因此,第二个线程的EnterCriticalSection(&cs)语句将不会返回,而处于挂起等待状态。直到第一个线程执行了LeaveCriticalSection(&cs),第二个线程的EnterCriticalSection(&cs)语句才会返回,并且继续执行下面的操作。

       这个过程实际上是通过限制有且只有一个函数进入CriticalSection变量来实现代码段同步的。简单地说,对于同一个CRITICAL_SECTION,当一个线程执行了EnterCriticalSection而没有执行LeaveCriticalSection的时候,其它任何一个线程都无法完全执行EnterCriticalSection而不得不处于等待状态。

再次强调一次,没有任何资源被“锁定”,CRITICAL_SECTION这个东东不是针对于资源的,而是针对于不同线程间的代码段的!我们能够用它来进行所谓资源的“锁定”,其实是因为我们在任何访问共享资源的地方都加入了EnterCriticalSectionLeaveCriticalSection语句,使得同一时间只能够有一个线程的代码段访问到该共享资源而已(其它想访问该资源的代码段不得不等待)。

如果是两个CRITICAL_SECTION,就以此类推。

 

再举个极端的例子,可以帮助你理解CRITICAL_SECTION这个东东:

第一个线程函数:

DWORD WINAPI ThreadFuncA(LPVOID lp)

{

EnterCriticalSection(&cs);

for(int i=0;i <1000;i++)

Sleep(1000);

LeaveCriticalSection(&cs);

return 0;

}

 

第二个线程函数:

DWORD WINAPI ThreadFuncB(LPVOID lp)

{

EnterCriticalSection(&cs);

index=2;

LeaveCriticalSection(&cs);

return 0;

}

      这种情况下,第一个线程中间总共Sleep1000秒钟!它显然没有对任何资源进行什么“有意识”的保护;而第二个线程是要访问资源index的,但是由于第一个线程占用了cs,一直没有Leave,而导致第二个线程不得不登上1000秒钟……

第二个线程,真是可怜哪。。。

这个应该很说明问题了,你会看到第二个线程在1000秒钟之后开始执行index=2这个语句。

也就是说,CRITICAL_SECTION其实并不理会你关心的具体共享资源,它只按照自己的规律办事~

 

 

函数模板

 

函数模板不是一个可以执行的函数,它只是对函数功能的程序描述,编译程序不为它生成执行代码。函数模板的声明格式是:

 

template <class 类型参数名1, class 类型参数名 2,>

函数返回值类型函数名(形参表)

{

  函数体;

}

 

下面举例说明创建和应用函数模板。

 

【例12-2-1】编写一个程序,使它能够输出不同类型数组中的数据。

 

一般情况下,我们会使用函数重载的方法实现:

 

参考源代码:

 

/* 12-2-112-2-1_1.cpp */

 

#include <iostream.h>

 

#include <stdlib.h>

 

using namespace std;

 

void outputArray(int *array, int count)

 

{

 

  for ( int i = 0; i < count; i++ )

 

  cout << array[i] << " ";

 

  cout << endl;

 

 }

 

void outputArray(double *array, int count)

 

{

 

  for ( int i = 0; i < count; i++ )

 

  cout << array[i] << " ";

 

  cout << endl;

 

}

 

void outputArray(char *array, int count)

 

{

 

  for ( int i = 0; i < count; i++ )

 

  cout << array[i] << " ";

 

  cout << endl;

 

}

 

void main()

 

{

 

  const int ac = 5, bc = 3, cc = 5;

 

  int a[ac] = { 1, 2, 3, 4, 5 };

 

  double b[bc] = { 5.1, 2.7, 4.9 };

 

  char c[cc] = "SCIT";

 

  cout << "Array a: ";

 

  outputArray(a, ac);             //输出数组a

 

  cout << "Array b: ";

 

  outputArray(b, bc);             //输出数组b

 

  cout << "Array c:" ;

 

  outputArray(c, cc);        //输出数组c

 

  system("pause");

 

}

 

运行结果:

 

Array a: 1      2    3     4     5

 

Array b: 5.1   2.7 4.9

 

Array c: S      C    I      T

 

引入函数模板后,我们就可以做如下修改:

 

第一步,写出其中的一个普通函数:

 

void outputArray(int *array,int count)

 

{

 

 for ( int i =0; i < count; i++ )

 

 cout <<array[i] << " ";

 

 cout <<endl;

 

}

 

第二步,将数据类型参数化,即把其中具体的数据类型名全部替换成由自己定义的抽象的类型参数名(T)

 

// 将原来的int改为T,注意,用类型参数替换的是数据类型名,不是变量名

 

void outputArray(T *array,int count)

 

{

 

  for (int  i = 0; i < count; i++ )

 

  cout<< array[i] << " ";

 

  cout<< endl;

 

}

 

第三步,使用关键字template声明类型参数名,并放在函数头前:

 

 template<class T>      //行末不要加分号!

 

 voidoutputArray(T *array,int count)

 

 {

 

   for ( int i= 0; i < count; i++ )

 

   cout<< array[i] << " ";

 

   cout<< endl;

 

 }

 

这样我们就把一个普通的函数修改成一个通用的函数模板。那么把它应用到【例12-2-1】,就可以写成如下程序,程序执行的输出与上同:

 

参考源代码:

 

/* 12-2-112-2-1_2.cpp */

 

#include <iostream.h>

 

#include <stdlib.h>

 

using namespace std;

 

template <class T>        //定义数组的通用函数模板outputArray()

 

void outputArray(T *array, int count)

 

{

 

  for( int i = 0; i < count; i++ )

 

 cout << array[i] << " ";

 

 cout << endl;

 

}

 

void main()

 

{

 

  const int ac = 5, bc = 3, cc = 5;

 

  int a[ac] = { 1, 2, 3, 4, 5, }; //数组初始化

 

  double b[bc] = { 5.1, 2.7, 4.9 };

 

  char c[cc] = "SCIT";

 

  cout << "Array a:"<< endl;

 

  outputArray(a, ac);      //输出数组a

 

  cout << "Array b:"<< endl;

 

  outputArray(b,bc);      //输出数组b

 

  cout << "Array c:" << endl;

 

  outputArray(c, cc);      //输出数组c

 

  system("pause");

 

}  

 

改写后,可以看到,通过创建一个数组的通用函数模板outputArray(),其类型参数名为T(代替了intdoublechar等普通的数据类型),系统就能根据主函数中函数调用的第一个实参的数据类型,由函数模板生成不同数据类型的函数。

 

现在,让我们了解模板函数的概念,以及函数模板与模板函数的关系。

 

通过前面的学习,我们已经了解到函数模板仅是对一组函数的描述,是一个函数模型,而不是一个实实在在的函数,只有当编译系统在程序中发现有与函数模板中相匹配的函数调用时生成一系列的重载函数,重载函数的函数体与函数模板的函数体相同。

 

 

WSAStartup(

 应用程序或DLL在使用Windows Sockets服务之前必须要进行一次成功的WSAStartup()调用.当它完成了Windows Sockets的使用后,应用程序或DLL必须调用WSACleanup()将其从Windows Sockets的实现中注销,并且该实现释放为应用程序或DLL分配的任何资源.任何打开的并已建立连接的SOCK_STREAM类型套接口在调用WSACleanup()时会重置;而已经由closesocket()关闭却仍有要发送的悬而未决数据的套接口则不会受影响-该数据仍要发送.

  对应于一个任务进行的每一次WSAStartup()调用,必须有一个WSACleanup()调用.只有最后的WSACleanup()做实际的清除工作;前面的调用仅仅将Windows Sockets DLL中的内置引用计数递减.一个简单的应用程序为确保WSACleanup()调用了足够的次数,可以在一个循环中不断调用WSACleanup()直至返回WSANOTINITIALISED.

  返回值:

  0操作成功.

  SOCKET_ERROR否则.同时可以调用WSAGetLastError()获得错误代码.


  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值