IOCP 用法
good doc
https://www.cnblogs.com/tanguoying/p/8439701.Html
Introduction
而传统的服务器端 / 客户端模式通常为每一个客户端创建一个独立的线程,这种实现方式在客户端数量不多的情况下问题不大,但对于类似于铁路网络订票这样的瞬间客户数量巨大的系统来说,效率极端低下。这是因为一方面创建新线程操作系统开销较大,另一方面同时有许多线程处于运行状态,操作系统内核需要花费大量时间进行上下文切换,并没有在线程执行上花更多的时间。
因此,微软在 Winsocket2 中引入了 IOCP(Input/Output Completion Port)模型。IOCP 是 Input/Output Completion Port(I/O 完成端口)的简称。简单的说,IOCP 是一种高性能的 I/O 模型,是一种应用程序使用线程池处理异步 I/O 请求的机制。Java7 中对 IOCP 有了很好的封装,程序员可以非常方便的时候经过封装的 channel 类来读写和传输数据。
Accept 后线程被挂起,等待一个客户发出请求,而后创建新线程来处理请求。当新线程处理客户请求时,起初的线程循环回去等待另个客户请求。在这个并发模型中,对每个客户都创建了一个线程。其优点在于等待请求的线程只需要做很少的工作,而大部分的时间,该线程在休眠,因为 recv 处于阻塞状态。如前文所述,创建线程的开销远远大于程序员的预计,尤其是在并发量巨大的情况下,这种传统的并发模型效率极端低下。
解决这个问题的方法之一就是 IOCP,说白了 IOCP 就是一个消息队列。我们设想一下,如果事先开好 N 个线程,让它们 hold 住,将所有用户的请求都投递到一个消息队列中去。让后这 N 个线程逐一从消息队列中去取出消息并加以处理。这样一来,就可以避免对没有用户请求都开新线程,不仅减少了线程的资源,也提高了线程的利用率。
IOCP 实现的基本步骤
那么 IOCP 完成端口模型又是怎样实现的呢?首先我们创建一个完成端口 CreateIOCompletionPort,然后再创建一个或多个工作线程,并指定它们到这个完成端口上去读取数据。再将远程连接的套接字句柄关联到这个完成端口。工作线程调用 getQueuedCompletionStatus 方法在关联到这个完成端口上的所有套接字上等待 I/O 的完成,再判断完成了什么类型的 I/O,然后接着发出 WSASend 和 WSARecv,并继续下一次循环阻塞在 getQueuedCompletionStatus。
创建一个完成端口;
Port port = createIoCompletionPort(INVALID_HANDLE_VALUE, 0, 0, fixedThreadCount());
创建一个线程 ThreadA;
ThreadA 线程循环调用 GetQueuedCompletionStatus 方法来得到 I/O 操作结果,这个方法是一个阻塞方法;
While(true){
getQueuedCompletionStatus(port, ioResult);
}
主线程循环调用 accept 等待客户端连接上来;
主线程 accept 返回新连接建立以后,把这个新的套接字句柄用 CreateIoCompletionPort 关联到完成端口,然后发出一个异步的 Read 或者 Write 调用,因为是异步函数,Read/Write 会马上返回,实际的发送或者接收数据的操作由操作系统去做。
if (handle != 0L) {
createIoCompletionPort(handle, port, key, 0);
}
主线程继续下一次循环,阻塞在 accept 这里等待客户端连接。
操作系统完成 Read 或者 Write 的操作,把结果发到完成端口。
ThreadA 线程里的 GetQueuedCompletionStatus() 马上返回,并从完成端口取得刚完成的 Read/Write 的结果。
在 ThreadA 线程里对这些数据进行处理 ( 如果处理过程很耗时,需要新开线程处理 ),然后接着发出 Read/Write,并继续下一次循环阻塞在 GetQueuedCompletionStatus() 这里。
IOCP 优点
优点:
① 帮助维持重复使用的内存池。(与重叠I/O技术有关)
② 去除删除线程创建/终结负担。
③ 利于管理,分配线程,控制并发,最小化的线程上下文切换。
④ 优化线程调度,提高CPU和内存缓冲的命中率。
缺点:
理解以及编码的复杂度较高。对使用者有一定要求。
需了解以下基本知识:
① 同步与异步
② 阻塞与非阻塞
③ 重叠I/O技术
④ 多线程
⑤ 栈、队列这两种基本的数据结构
相关API
① 与SOCKET相关
1、链接套接字动态链接库:int WSAStartup(…);
2、创建套接字库: SOCKET socket(…);
3、绑字套接字: int bind(…);
4、套接字设为监听状态: int listen(…);
5、接收套接字: SOCKET accept(…);
6、向指定套接字发送信息:int send(…);
7、从指定套接字接收信息:int recv(…);[1]
② 与线程相关
1、创建线程:HANDLE CreateThread(…);
③ 重叠I/O技术相关
1、向套接字发送数据: int WSASend(…);
2、向套接字发送数据包: int WSASendTo(…);
3、从套接字接收数据: int WSARecv(…);
4、从套接字接收数据包: int WSARecvFrom(…);
④ IOCP相关
1、创建/关联完成端口: HANDLE WINAPI CreateIoCompletionPort(…);
2、获取队列完成状态: BOOL WINAPI GetQueuedCompletionStatus(…);
3、投递一个队列完成状态:BOOL WINAPI PostQueuedCompletionStatus(…);
重叠IO
重叠IO[overlapped I/O]是Win32的一项技术,你可以要求操作系统为你传送数据,并且在传送完毕时通知你。这也就是[完成]的含义。这项技术使你的程序在I/O进行过程中仍然能够继续处理事务。事实上,操作系统内部正是以线程来完成overlapped I/O。你可以获得线程所有利益,而不需要付出什么痛苦的代价。
IOCP 理解1
IOCP从本质上来说,没什么复杂的,抛开异步IO(这个是系统本身的IO,至少我没法实现),我们可以自己设计一个类似这样的东西,而且非常之简单。用过IOCP的人都应该熟悉这两个函数:PostQueueCompletionStatus, GetQueueCompletionStatus。用第一个函数Post给IOCP的数据,可以用第二个函数Get出来,其实我们就可以用一个信号量加一个队列和一个临界区就可以实现,队列为空的时候,信号量为0,Get函数用WaitForSingleobject阻塞在信号量上,往队列里Post数据的时候,先将数据插入到队列尾,然后Release一个信号量,那边阻塞住的Get函数从WaitForSingleObject那里返回,返回后就从队列里取一个数据,就如此简单。
在UNIX/LINUX下,也有这种东西,就是消息队列,System V和Posix都有消息队列,几乎和PostQueueCompletionStatus, GetQueueCompletionStatus一模一样,稍微有点不同的地方,就是UNIX/LINUX下的消息队列都带有一个优先级。对Posix消息队列来说,返回的总是优先级最高的消息,而对System V来说,是任意的。现在最新的LINUX下的异步IO(这里我要特别强调一下异步IO和非阻塞IO的区别,异步IO就是把IO提交给系统,让系统替你做,做完了再用某种方式通知你;非阻塞IO就是你要通过某种方式不定时地向系统询问你是否可以开始做某个IO,当可以开始后,还是要自己来完成IO)据说性能很强,不过我没用过,昨天晚上我在考虑,是否可以用异步IO加上消息队列在LINUX下实现一个类似WINDOWS下的IOCP的东西,这样对于很多从WINDOWS下转过来的程序员就会上手很快。目前EPOLL根据我的这几天研究,发现EPOLL实质上就是POLL的演化,用非阻塞IO来实现的,总感觉不是很舒服,毕竟异步IO是系统来完成IO,肯定高效得多。
IOCP 线程池
当线程池中的一个线程调用GetQueuedCompletionStatus时,调用线程的ID就会被放入该等待线程队列中.这样,IOCP就知道哪个线程在等待完成I/O的请求.对于IOCP来说,创建一定数量的工作线程是必要的,但不是必须预先创建。通常的资料中会提示您创建cpu核心数x2+2数量的工作者线程。但是请注意:这只是一般情况。 一个线程执行效率是否够高,要看它做了什么和等待了什么。线程在执行一些阻塞操作的时候是会进行等待的,比如等待硬盘I/O,等待串口数据,等待SQL查询结果……在工作线程被阻塞并等待时,操作系统会切换context,选择并将执行权力交给另一个“饥饿”线程。试想一个场景:您的IOCP系统启动时创建了10个工作线程,当某一时刻(比如过年大家都在发短信,而您的系统需要通过串口向短信modem发送和接收数据)大量的需要进行串口数据收发的请求被连入系统,所有10个工作线程全部被排队阻塞在串口短信机收发动作上,这时新的请求就无法及时处理,但您系统的CPU资源其实还有很大富余。只是能够对数据进行处理的工作者线程已经被耗尽。 这个情况该怎么处理呢? 这时您就应当考虑“线程池”的管理模式了。由线程池管理者判断当前工作线程是否还有空闲的(GetQueuedCompletionStatus时应属于空闲)。如果空闲线程已经不足(这个取决于逻辑,比如当前只有0个或2个空闲线程),那么管理者应立即创建新的工作者线程。如果空闲线程太多(比如有20个空闲的),管理者应将多余的线程关闭(PostQueuedCompletionStatus(MY_MSG_EXIT))。 至于您的疑惑:在用IOCP的时候不就是要预先创建一定数量的线程吗,一旦这些线程全部创建完毕,是不是就等价于已经创建了一个线程池了?预先创建并且不能够根据实际需要增加和减少的这些工作线程,只能称为线程组,它们并不是线程池。另外,IOCP也并不是必须在启动时创建一定数量工作线程的,完全可以全部都交给线程池进行判断和管理。启动后当线程池判断空闲工作者不足,则立即进行创建动作。
IOCP 函数解析
CreateIoCompletionPort
1 | HANDLE completionPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0); |
CreateIoCompletionPort函数有两个功能:
- 创建一个完成端口
- 将一个句柄关联到完成端口
我们创建时给的参数是(INVALID_HANDLE_VALUE, NULL, 0, 0)就是创建完成端口,下面会介绍关联完成端口。
建立Worker线程
我们最好是建立CPU核心数量2那么多的线程,这样更可以充分利用CPU资源,*因为完成端口的调度是非常智能的,比如我们的Worker线程有的时候可能会有Sleep()或者WaitForSingleObject()之类的情况,这样同一个CPU核心上的另一个线程就可以代替这个Sleep的线程执行了;因为完成端口的目标是要使得CPU满负荷的工作。**
工作线程所要做的工作就是几个Worker线程哥几个一起排好队队来监视完成端口的队列中是否有完成的网络操作就好了
GetQueuedCompletionStatus
实现从指定的IOCP获取CP。当CP队列为空时,对此函数的调用将被阻塞,而不是一直等待 I/O的完成。当CP队列不为空时,被阻塞的线程将以后进先出(LIFO)顺序被释放。对于IOCP机制,它允许多线程并发调用 GetQueuedCompletionStatus函数,最大并发数是在调用CreateIoCompletionPort函数时指定的,超出最大并发 数的调用线程,将被阻塞。
其中的GetQueuedCompletionStatus()就是Worker线程里第一件也是最重要的一件事了,
会让Worker线程进入不占用CPU的睡眠状态,直到完成端口上出现了需要处理的网络操作或者超出了等待的时间限制为止。
一旦完成端口上出现了已完成的I/O请求,那么等待的线程会被立刻唤醒,然后继续执行后续的代码。
1 | BOOL WINAPI GetQueuedCompletionStatus( |
返回值:
调用成功,则返回非零数值,相关数据存于lpNumberOfBytes、lpCompletionKey、lpCompletionKey变量中。失败则返回零值。
如果这个函数突然返回了,那就说明有需要处理的网络操作了 — 当然,在没有出现错误的情况下。 然后switch()一下,根据需要处理的操作类型,那我们来进行相应的处理。
那我们如何直到需要处理的操作类型呢?这个就要用到我们定义的IOContext类,里面有一个WSAOVERLAPPED的变量和操作类型(参见第3步)。那有如何吧IOContext变量传进来呢?同样参见第三步我们投递AcceptEx请求时传入了一个&ioContext->overLapped参数。我们可以使用PER_IO_CONTEXT这个宏来通过ioContext->overLapped取得ioContext的地址,如此我们便取得操作类型和ioContext中的WSAbuf。数据就存放在WSABuf中。
另外,我们注意到关联socket到完成端口时,我们给CreateIoCompletionPort()函数的第三个参数ULONG_PTR CompletionKey参数传递了listenSockContext变量,我们可以在GetQueuedCompletionStatus的第三个参数取得这个传进来的变量。如此我们就通过完成端口穿进去了两个变量,理解这两个变量的传递时理解完成端口模式的关键,我之前就时卡着这里。
PostQueuedCompletionStatus
Worker线程一旦进入了GetQueuedCompletionStatus()的阶段,就会进入睡眠状态,INFINITE的等待完成端口中,如果完成端口上一直都没有已经完成的I/O请求,那么这些线程将无法被唤醒,这也意味着线程没法正常退出。
熟悉或者不熟悉多线程编程的朋友,都应该知道,如果在线程睡眠的时候,简单粗暴的就把线程关闭掉的话,那是会一个很可怕的事情,因为很多线程体内很多资源都来不及释放掉,无论是这些资源最后是否会被操作系统回收,我们作为一个C++程序员来讲,都不应该允许这样的事情出现。
所以我们必须得有一个很优雅的,让线程自己退出的办法。
这时会用到我们这次见到的与完成端口有关的最后一个API,叫 PostQueuedCompletionStatus(),从名字上也能看得出来,这个是和 GetQueuedCompletionStatus() 函数相对的,这个函数的用途就是可以让我们手动的添加一个完成端口I/O操作,这样处于睡眠等待的状态的线程就会有一个被唤醒,如果为我们每一个Worker线程都调用一次PostQueuedCompletionStatus()的话,那么所有的线程也就会因此而被唤醒了。
PostQueuedCompletionStatus()函数的原型是这样定义的:
1 | BOOL WINAPI PostQueuedCompletionStatus( |
我们可以看到,这个函数的参数几乎和GetQueuedCompletionStatus()的一模一样,都是需要把我们建立的完成端口传进去,然后后面的三个参数是 传输字节数、结构体参数、重叠结构的指针.
注意,这里也有一个很神奇的事情,正常情况下,GetQueuedCompletionStatus()获取回来的参数本来是应该是系统帮我们填充的,或者是在绑定完成端口时就有的,但是我们这里却可以直接使用PostQueuedCompletionStatus()直接将后面三个参数传递给GetQueuedCompletionStatus(),这样就非常方便了。
例如,我们为了能够实现通知线程退出的效果,可以自己定义一些约定,比如把这后面三个参数设置一个特殊的值,然后Worker线程接收到完成通知之后,通过判断这3个参数中是否出现了特殊的值,来决定是否是应该退出线程了。
IOCP 线程调度
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 wshten@gmail.com
文章标题:IOCP 用法
本文作者:KevinTen
发布时间:2019-10-09, 00:00:00
最后更新:2019-10-14, 21:12:32
原始链接:http://github.com/kevinten10/2019/10/09/Science/IO-IOCP/版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。