sherrylso

C++博客 首页 新随笔 联系 聚合 管理
  18 Posts :: 0 Stories :: 124 Comments :: 0 Trackbacks

         本文主要探讨一下windows平台上的完成端口开发及其与之相关的几个重要的技术概念,这些概念都是与基于IOCP的开发密切相关的,对开发人员来讲,又不得不给予足够重视的几个概念:
1) 基于IOCP实现的服务吞吐量
2)IOCP模式下的线程切换
3)基于IOCP实现的消息的乱序问题。

一、IOCP简介
    提到IOCP,大家都非常熟悉,其基本的编程模式,我就不在这里展开了。在这里我主要是把IOCP中所提及的概念做一个基本性的总结。IOCP的基本架构图如下:
 

如图所示:在IOCP中,主要有以下的参与者:
--》完成端口:是一个FIFO队列,操作系统的IO子系统在IO操作完成后,会把相应的IO packet放入该队列。
--》等待者线程队列:通过调用GetQueuedCompletionStatus API,在完成端口上等待取下一个IO packet。
--》执行者线程组:已经从完成端口上获得IO packet,在占用CPU进行处理。
除了以上三种类型的参与者。我们还应该注意两个关联关系,即:
--》IO Handle与完成端口相关联:任何期望使用IOCP的方式来处理IO请求的,必须将相应的IO Handle与该完成端口相关联。需要指出的时,这里的IO Handle,可以是File的Handle,或者是Socket的Handle。
--》线程与完成端口相关联:任何调用GetQueuedCompletionStatus API的线程,都将与该完成端口相关联。在任何给定的时候,该线程只能与一个完成端口相关联,与最后一次调用的GetQueuedCompletionStatus为准。
二、高并发的服务器(基于socket)实现方法
        一般来讲,实现基于socket的服务器,有三种实现的方式(thread per request的方式,我就不提了:)):
第一、线程池的方式。使用线程池来对客户端请求进行服务。使用这种方式时,当客户端对服务器的连接是短连接(所谓的短连接,即:客户端对服务器不是长时间连接)时,是可以考虑的。但是,如若客户端对服务器的连接是长连接时,我们需要限制服务器端的最大连接数目为线程池线程的最大数目,而这应用的设计本身来讲,是不好的设计方式,scalability会存在问题。
第二、基于Select的服务器实现。其本质是,使用Select(操作系统提供的API)来监视连接是否可读,可写,或者是否出错。相比于前一种方式,Select允许应用使用一个线程(或者是有限几个线程)来监视连接的可读写性。当有连接可读可写时,应用可以以non-bolock的方式读写socket上的数据。使用Select的方式的缺点是,当Select所监视的连接数目在千的数量级时,性能会打折扣。这是因为操作系统内核需要在内部对这些Socket进行轮询,以检查其可读写性。另一个问题是:应用必须在处理完所有的可读写socket的IO请求之后,才能再次调用Select,进行下一轮的检查,否则会有潜在的问题。这样,造成的结果是,对一些请求的处理会出现饥饿的现象。
        一般common的做法是Select结合Leader-Follower设计模式使用。不过不管怎样,Select的本质造成了其在Scalability的问题是不如IOCP,这也是很多high-scalabe的服务器采用IOCP的原因。
第三、IOCP实现高并发的服务器。IOCP是实现high-scalabe的服务器的首选。其特点我们专门在下一小姐陈述。
三、IOCP开发的几个概念
第一、服务器的吞吐量问题。
      我们都知道,基于IOCP的开发是异步IO的,也正是这一技术的本质,决定了IOCP所实现的服务器的高吞吐量。
       我们举一个及其简化的例子,来说明这一问题。在网络服务器的开发过程中,影响其性能吞吐量的,有很多因素,在这里,我们只是把关注点放在两个方面,即:网络IO速度与Disk IO速度。我们假设:在一个千兆的网络环境下,我们的网络传输速度的极限是大概125M/s,而Disk IO的速度是10M/s。在这样的前提下,慢速的Disk 设备会成为我们整个应用的瓶颈。我们假设线程A负责从网络上读取数据,然后将这些数据写入Disk。如果对Disk的写入是同步的,那么线程A在等待写完Disk的过程是不能再从网络上接受数据的,在写入Disk的时间内,我们可以认为这时候Server的吞吐量为0(没有接受新的客户端请求)。对于这样的同步读写Disk,一些的解决方案是通过增加线程数来增加服务器处理的吞吐量,即:当线程A从网络上接受数据后,驱动另外单独的线程来完成读写Disk任务。这样的方案缺点是:需要线程间的合作,需要线程间的切换(这是另一个我们要讨论的问题)。而IOCP的异步IO本质,就是通过操作系统内核的支持,允许线程A以非阻塞的方式向IO子系统投递IO请求,而后马上从网络上读取下一个客户端请求。这样,结果是:在不增加线程数的情况下,IOCP大大增加了服务器的吞吐量。说到这里,听起来感觉很像是DMA。的确,许多软件的实现技术,在本质上,与硬件的实现技术是相通的。另外一个典型的例子是硬件的流水线技术,同样,在软件领域,也有很著名的应用。好像话题扯远了,呵呵:)
第二、线程间的切换问题。
         服务器的实现,通过引入IOCP,会大大减少Thread切换带来的额外开销。我们都知道,对于服务器性能的一个重要的评估指标就是:System\Context Switches,即单位时间内线程的切换次数。如果在每秒内,线程的切换次数在千的数量级上,这就意味着你的服务器性能值得商榷。Context Switches/s应该越小越好。说到这里,我们来重新审视一下IOCP。
     完成端口的线程并发量可以在创建该完成端口时指定(即NumberOfConcurrentThreads参数)。该并发量限制了与该完成端口相关联的可运行线程的数目(就是前面我在IOCP简介中提到的执行者线程组的最大数目)。当与该完成端口相关联的可运行线程的总数目达到了该并发量,系统就会阻塞任何与该完成端口相关联的后续线程的执行,直到与该完成端口相关联的可运行线程数目下降到小于该并发量为止。最有效的假想是发生在有完成包在队列中等待,而没有等待被满足,因为此时完成端口达到了其并发量的极限。此时,一个正在运行中的线程调用GetQueuedCompletionStatus时,它就会立刻从队列中取走该完成包。这样就不存在着环境的切换,因为该处于运行中的线程就会连续不断地从队列中取走完成包,而其他的线程就不能运行了。
     完成端口的线程并发量的建议值就是你系统CPU的数目。在这里,要区分清楚的是,完成端口的线程并发量与你为完成端口创建的工作者线程数是没有任何关系的,工作者线程数的数目,完全取决于你的整个应用的设计(当然这个不宜过大,否则失去了IOCP的本意:))。
第三、IOCP开发过程中的消息乱序问题。
     使用IOCP开发的问题在于它的复杂。我们都知道,在使用TCP时,TCP协议本身保证了消息传递的次序性,这大大降低了上层应用的复杂性。但是当使用IOCP时,问题就不再那么简单。如下例:
  
       三个线程同时从IOCP中读取Msg1, Msg2,与Msg3。由于TCP本身消息传递的有序性,所以,在IOCP队列内,Msg1-Msg2-Msg3保证了有序性。三个线程分别从IOCP中取出Msg1,Msg2与Msg3,然后三个线程都会将各自取到的消息投递到逻辑层处理。在逻辑处理层的实现,我们不应该假定Msg1-Msg2-Msg3顺序,原因其实很简单,在Time 1~Time 2的时间段内,三个线程被操作系统调度的先后次序是不确定的,所以在到达逻辑处理层,
Msg1,Msg2与Msg3的次序也就是不确定的。所以,逻辑处理层的实现,必须考虑消息乱序的情况,必须考虑多线程环境下的程序实现。
        在这里,我把消息乱序的问题单列了出来。其实在IOCP的开发过程中,相比于同步的方式,应该还有其它更多的难题需要解决,这也是与Select方式相比,IOCP的缺点,实现复杂度高。
结束语:
    ACE的Proactor Framework, 对windows平台的IOCP做了基于Proactor设计模式的,面向对象的封装,这在一定程度上简化了应用开发的难度,是一个很好的异步IO的开发框架,推荐学习使用。 
Reference:
    Microsoft Technet,Inside I/O Completion Ports

posted on 2007-08-26 16:06 爱上龙卷风 阅读(16362) 评论(23)  编辑 收藏 引用

Feedback

# re: 完成端口(IOCP)编程探讨[未登录] 2007-09-05 07:15 小明
对于你提出的第三个问题,乱序问题

一般写IOCP程序,都会让每个socket和一个buffer关联
只要让WSARecv保证每次按次序的写到buffer
然后逻辑层也按照次序的来读取信息(需要有锁来控制)
这样就不会有乱序问题了。

不知道你对于这个问题有什么高招?  回复  更多评论
  

# re: 完成端口(IOCP)编程探讨 2007-09-06 10:11 NULL
回答下上面的问题,你的问题就有问题,如果只有一个recv投递,那就不会存在乱序的问题,如果你投递n个,按顺序1.2.3,但是你怎么保证recv不是按照2.3.1返回的呢?  回复  更多评论
  

# re: 完成端口(IOCP)编程探讨 2007-10-20 15:30 RedFox
我也是進來討論你提出的第三個問題的。

通常的做法是一個 Socket 用一個 WSARecv 異步操作,有完成事件時,先處理完這個異步操作時,才再次發出下一個 WSARecv 異步操作。這樣就不會有你說的消息亂序問題了。

而你說的一次對同一個 socket 提交 n 個 WSARecv 操作。我認為是不可取的。
  回复  更多评论
  

# re: 完成端口(IOCP)编程探讨 2007-10-21 20:15 爱上龙卷风
@RedFox
你应该弄错了,的确,如果按照你的说法,就是单线程了,没有多线程。
当然没有问题。但是,你的performance是没法保证的,几乎没有什么
throughput的。
  回复  更多评论
  

# re: 完成端口(IOCP)编程探讨 2007-10-21 20:17 爱上龙卷风
@小明
对于你的问题,我同意NULL的解释  回复  更多评论
  

# re: 完成端口(IOCP)编程探讨 2007-10-22 08:41 RedFox
可能是我水平不夠。
的確,我是對每個 socket 做一個 WSARecv. 處理完這個 WSARecv 後我再發起下一個 WSARecv. 如果有 1000個連接,而 IOCP 裡就有 1000個 IO請求包。

而你說的一個 socket 就發起 N 個 WSARecv. 首先,讀到的數據次序不能保證,再次,如果 socket 關閉時,你也會收到 N 個關閉消息,你應該在哪個關閉消息時去釋放資源呢?  回复  更多评论
  

# re: 完成端口(IOCP)编程探讨 2007-11-11 14:19 爱上龙卷风
@RedFox
你说的收到N 個關閉消息,指的是你自己定义的吗?
不管怎么样,引用计数技术应该能帮到你。  回复  更多评论
  

# re: 完成端口(IOCP)编程探讨 2008-01-07 17:12 唐新发
错别字:小组不是小姐
第三、IOCP实现高并发的服务器。IOCP是实现high-scalabe的服务器的首选。其特点我们专门在下一小姐陈述。  回复  更多评论
  

# re: 完成端口(IOCP)编程探讨 2008-01-07 23:08 abettor.org
@RedFox
我赞同你的看法。
但是,是否有更高效的办法呢?
我觉得如果业务逻辑层速度很慢的化,可以把按上述方法WSARecv到的数据再放到一个池里缓冲一下。不知道会不会适得其反。  回复  更多评论
  

# re: 完成端口(IOCP)编程探讨 2008-01-11 22:09 爱上龙卷风
@abettor.org
应该不是业务逻辑层速度很慢的问题,cpu的指令执行都是很快的,除非你访问慢速的IO设备。
对于业务逻辑层,应该提高的是并发的吞吐量。

  回复  更多评论
  

# re: 完成端口(IOCP)编程探讨 2008-03-31 09:05 RedFox
@abettor.org

IOCP 只處理封包,收到一個完整的包後,就POST到工作線程池,由工作線程池進行分配線程處理  回复  更多评论
  

# re: 完成端口(IOCP)编程探讨 2008-05-24 23:46 iunknown
@爱上龙卷风
你应该弄错了,的确,如果按照你的说法,就是单线程了,没有多线程。
当然没有问题。但是,你的performance是没法保证的,几乎没有什么
throughput的。


关于这一点,有实现过一个用单线程负责 IO 操作,线程池负责业务逻辑处理的 server framework 。负责 IO 的单线程,主要是执行 WSARecv, WSASend ,GetQueuedCompletionPort,具体的业务逻辑不在这个线程里面执行。
用这个框架实现过一个 echo server ,进行大压力测试,发现 throughput 还是很高的。可以认为在目前的 CPU 速度下,只是执行 WSARecv,WSASend,GetQueuedCompletionPort ,还是足够快的。
如果在同一台机运行 client 和 server,测试的结果是可以达到每秒 send 10万, recv 10万。  回复  更多评论
  

# re: 完成端口(IOCP)编程探讨 2008-05-24 23:49 iunknown
@RedFox
我也是進來討論你提出的第三個問題的。

通常的做法是一個 Socket 用一個 WSARecv 異步操作,有完成事件時,先處理完這個異步操作時,才再次發出下一個 WSARecv 異步操作。這樣就不會有你說的消息亂序問題了。

而你說的一次對同一個 socket 提交 n 個 WSARecv 操作。我認為是不可取的。


也是用这个方法来实现的,这样做会比较简单,可以避免消息的乱序问题。而且在关闭 socket 释放资源的时候,也相对容易控制。  回复  更多评论
  

# re: 完成端口(IOCP)编程探讨 2008-05-29 16:56 爱上龙卷风
@iunknown
关于你提到的框架,本质上是多线线程的。负责 IO 的线程数目理论上本来就应该是你机器cpu的个数。前提是你保证你的io设计是异步执行的。  回复  更多评论
  

# re: 完成端口(IOCP)编程探讨 2008-06-09 17:14 iunknown
@爱上龙卷风
>>>关于你提到的框架,本质上是多线线程的。负责 IO 的线程数目理论上本来就应该是你机器cpu的个数。前提是你保证你的io设计是异步执行的。

框架中用到了多线程,不过用于处理 IOCP 的线程(或者称为用于处理前端网络 IO 的线程)就只有一个。后端的线程池是用于处理具体的业务的,不会涉及前端的网络 IO 的。前端的网络 IO 可以非常清晰地剥离出来,由一个单独的线程来负责。在 input 的时候,在前端的 IO 线程中读入数据,放入 inputqueue,线程池负责 inputqueue;output 的时候,线程池把结果放入 outputqueue ,前端 IO 线程负责处理 outputqueue 。  回复  更多评论
  

# re: 完成端口(IOCP)编程探讨 2008-06-09 17:19 iunknown
这里有一个用 zero byte buffer 策略集成 iocp 到 libevent 的方案。
具体的源代码:http://spserver.googlecode.com/files/libevent-1.4.4-iocp-3.zip

vc6 的 dsw 文件在 libevent-1.4.4-iocp-3/libevent-iocp 目录中。
这个方案就是单线程负责 IO ,里面有 echo_iocp.exe 和 testiocpstress.exe ,可以试试看它的性能。

方案的描述可以看看
http://monkeymail.org/archives/libevent-users/2008-June/001269.html  回复  更多评论
  

# re: 完成端口(IOCP)编程探讨 2008-06-23 23:41 饭中淹
多个recv可以在overlapped上进行顺序绑定
recv请求的顺序,决定了它收到的数据的先后顺序。
只要进行简单的排序,就可以保证recv的数据的顺序正确。

如果用多个recv的话,可以直接去掉recvbuf了。对于一个socket,去掉recvbuf相当于是减少了内存复制,因为Io层会把WSARecv进去的buf拿来直接作为缓冲来保存收到的数据。

如果不去掉recvbuf,那么多recv就看起来不是那么有意义了。



另外,iocp的程序可以写的比较Open一点,不要老想着节省内存。对于buffer和overlapped结构,可以分别用一个池来管理,而不是固定写死。这样不需要等待处理完一次受到的buffer,就能紧接着发出下一次recv,而且在这个过程中overlapped这个结构还能够重用,效率自然就会提升的。




  回复  更多评论
  

# re: 完成端口(IOCP)编程探讨 2008-10-23 17:15 r2100
@NULL
回答下上面的问题,你的问题就有问题,如果只有一个recv投递,那就不会存在乱序的问题,如果你投递n个,按顺序1.2.3,但是你怎么保证recv不是按照2.3.1返回的呢?
分别给1.2.3的overlapped做上记号1.2.3,返回时加个排序就可以了。

  回复  更多评论
  

# re: 完成端口(IOCP)编程探讨 2009-10-19 17:42 @jurver
我同意@RedFox的观点。
1.给一个socket投递多个Recv,只有在数据需要复制到自己的缓冲区(有可能有多个缓冲区)的情况。多数情况下并不需要。对一个socket投递多个recv请求。由于接收到的数据也是按发送的顺序,而完成端口是按队列来处理的。所以接收的数据是按投递的接收请求复制到自己的缓冲区。举例:你的本意是接受1.“hello”;2."MM";3."GG".而假设发送的顺序是3;2;1;,那么第一个recv原本希望得到1 "hello",可是现在得到了 3“GG”。就会和你本意相差甚远,所以多个Recv投递是不理想的。根本不可取。还不如接收到数据,复制出来。再接收。  回复  更多评论
  

# re: 完成端口(IOCP)编程探讨[未登录] 2010-01-29 18:06 wang
楼主的设计 优点是可以增大吞吐量
拥有多个挂起的读操作增加了服务器的吞吐量,这是因为TCP/IP包将会被直接写到我们传入的buffer而不是TCP/IP栈(不会有双缓冲)。

其它同学的设计也有优点,采用什么样的设计 取决于你的业务,

但如何 避免乱序呢? 我想到一个办法 每个挂起的读 都有一个序列号,读成功后按照这些序列号 再排序 组合包 。

欢迎探讨 wanglovec@163.com QQ: 50713022  回复  更多评论
  

# re: 完成端口(IOCP)编程探讨 2011-04-06 17:37 Geph0rce
@唐新发
小节  回复  更多评论
  

# re: 完成端口(IOCP)编程探讨[未登录] 2012-07-04 16:10 1
good  回复  更多评论
  

# re: 完成端口(IOCP)编程探讨 2012-12-25 14:06 ILOLI
@@jurver
你这样收到数据乱序是发送端的问题,和接收端没有半毛钱的关系。  回复  更多评论
  


只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理