一、IOCP简介
IOCP(I/O Completion Port,I/O完成端口)是Windows操作系统中伸缩性最好的一种I/O模型。
I/O 完成端口是应用程序使用线程池处理异步 I/O 请求的一种机制。处理多个并发异步I/O请求时,使用 I/O 完成端口比在 I/O 请求时创建线程更快更高效。
二、IOCP的优势
I/O 完成端口可以充分利用 Windows 内核来进行 I/O 调度,相较于传统的 Winsock 模型,IOCP 在机制上有明显的优势。
模型 | 机制 | 特性 |
---|---|---|
select模型 | 通过select函数来管理I/O,可以确定一个或多个套接字的状态 | 该模型的优势是程序能够在单个线程内同时处理多个套接字连接,避免了阻塞模式下的线程膨胀 |
WSAAsyncSelect模型 | WSAAsyncSelect函数把socket设为非阻塞模式,并为socket绑定一个窗口句柄,依靠Windows的消息驱动机制,通过窗口进行消息接收、事件处理 | 该模型最突出的特点是与Windows的消息驱动机制融合在一起,使得开发带GUI界面的网络程序更简单 |
WSAEventSelect模型 | 该模型与WSAAsyncSelect模型类似,允许应用程序在一个或多个socket上接收基于事件的网络通知,不过该模型是经由事件对象句柄通知的 | 该模型简单易用,也不需要窗口环境,缺点是最多等待64个事件对象的限制,当socket连接数量增加时,必须创建多个线程来处理I/O |
重叠I/O模型 | 该模型引入了重叠数据结构,允许应用程序使用重叠结构一次投递一个或多个异步I/O请求 | 该模型使用Winsock 2.0库的API,如:WSASend、WSARecv等,真正做到了“异步处理” |
IOCP模型 | IOCP模型通过socket绑定完成端口,在socket上投递事件,工作线程在完成端口上轮询接收、处理事件 | IOCP充分利用内核对象的调度,只使用少量的几个线程来处理所有网络通信,消除了无谓的线程上下文切换,最大限度地提高了网络通信的性能 |
相较于传统的Winsock模型,IOCP的优势主要体现在两方面:独特的异步I/O方式和优秀的线程调度机制。
独特的异步I/O方式
IOCP模型在异步通信方式的基础上,设计了一套能够充分利用Windows内核的I/O通信机制,主要过程为:① socket关联iocp,② 在socket上投递I/O请求,③ 事件完成返回完成通知封包,④ 工作线程在iocp上处理事件。
IOCP的这种工作模式:程序只需要把事件投递出去,事件交给操作系统完成后,工作线程在完成端口上轮询处理。该模式充分利用了异步模式高速率输入输出的优势,能够有效提高程序的工作效率。
优秀的线程调度机制
完成端口可以抽象为一个公共消息队列,当用户请求到达时,完成端口把这些请求加入其抽象出的公共消息队列。这一过程与多个工作线程轮询消息队列并从中取出消息加以处理是并发操作。这种方式很好地实现了异步通信和负载均衡,因为它使几个线程“公平地”处理多客户端的I/O,并且线程空闲时会被挂起,不会占用CPU周期。
IOCP模型充分利用Windows系统内核,可以实现仅用少量的几个线程来处理和多个client之间的所有通信,消除了无谓的线程上下文切换,最大限度的提高了网络通信的性能。
三、IOCP的使用
初次学习使用IOCP的朋友在熟悉各个API时,建议参看MSDN的官方文档MSDN
IOCP的使用主要分为以下几步:
- 创建完成端口(iocp)对象
- 创建一个或多个工作线程,在完成端口上执行并处理投递到完成端口上的I/O请求
- Socket关联iocp对象,在Socket上投递网络事件
- 工作线程调用GetQueuedCompletionStatus函数获取完成通知封包,取得事件信息并进行处理
1 创建完成端口对象
使用IOCP模型,首先要调用 CreateIoCompletionPort 函数创建一个完成端口对象,Winsock将使用这个对象为任意数量的套接字句柄管理 I/O 请求。函数定义如下:
HANDLE WINAPI CreateIoCompletionPort(
_In_ HANDLE FileHandle,
_In_opt_ HANDLE ExistingCompletionPort,
_In_ ULONG_PTR CompletionKey,
_In_ DWORD NumberOfConcurrentThreads
);
此函数的两个不同功能:
- 创建一个完成端口对象
- 将一个或多个文件句柄(这里是套接字句柄)关联到 I/O 完成端口对象
最初创建完成端口对象时,唯一需要设置的参数是 NumberOfConcurrentThreads,该参数定义了 允许在完成端口上同时执行的线程的数量。理想情况下,我们希望每个处理器仅运行一个线程来为完成端口提供服务,以避免线程上下文切换。NumberOfConcurrentThreads 为0表示系统允许的线程数量和处理器数量一样多。因此,可以简单地使用以下代码创建完成端口对象,取得标识完成端口的句柄。
HANDLE m_hCompletion = ::CreateIoCompletionPort(INVALID_HANDLE_VALUE,0,0,0);
2 I/O工作线程和完成端口
I/O 工作线程在完成端口上执行并处理投递的I/O请求。关于工作线程的数量,要注意的是,创建完成端口时指定的线程数量和这里要创建的线程数量不是一回事。CreateIoCompletionPort 函数的 NumberOfConcurrentThreads 参数明确告诉系统允许在完成端口上同时运行的线程数量。如果创建的线程数量多于 NumberOfConcurrentThreads,也仅有NumberOfConcurrentThreads 个线程允许运行。
但也存在确实需要创建更多线程的特殊情况,这主要取决于程序的总体设计。如果某个线程调用了一个函数,如 Sleep 或 WaitForSingleObject,进入了暂停状态,多出来的线程中就会有一个开始运行,占据休眠线程的位置。
有了足够的工作线程来处理完成端口上的 I/O 请求后,就该为完成端口关联套接字句柄了,这就用到了 CreateCompletionPort 函数的前3个参数。
FileHandle:要关联的套接字句柄
ExistingCompletionPort:要关联的完成端口对象句柄
CompletionKey:指定一个句柄唯一(per-handle)数据,它将与FileHandle套接字句柄关联在一起