找回密码
 用户注册

QQ登录

只需一步,快速开始

楼主: wishel

打造Epoll Proactor

[复制链接]
 楼主| 发表于 2009-11-23 17:58:56 | 显示全部楼层
原帖由 modern 于 2009-11-23 09:16 发表
周末简单的看了wishel的代码,思路很不错,而且代码也十分工整。
1、从代码分析目前压力应该还是再I/O读写,IOCP的I/O读写由windows内核帮忙完成,
显然效率是很高的,而目前这套框架是由运行Proactor的单线程负责I/O读写,
似乎不能完全发挥EPoll的威力,期待Wishel的LF线程池版本。
2、Handle_Event调度的时候,个人认为顺序可以考虑调整一下,优先级错误>写>读,
是否效果更好一些。

谢谢modern认真帮我分析。我现在的想法:

1,内核做同样的事情不一定会比user态就快。比如1+1,核心态或者是用户态运行效率没有什么区别。
有区别的情况是有的,比如在某些情况下内核态在内存分配、访问等情况下性能要好一些。(例如windows的内存管理,在核心是non-paged pool,也就是说不会被交换到swap。我初学,不太确认啊。)
所以个人认为应该具体情况具体分析,现在这种io,内核没道理一定就比用户态快。除非能证明用户态比内核态多了某些显著成本。

2,原程序考虑不周,其实即使是oneshot,同一handle,错误,写,读 ready的情况是可能同时发生的。
所以现在改了下,不再用优先级,顺序全部处理。也就是不用if。。else if。。,改为if。。,if 。。

[ 本帖最后由 wishel 于 2009-11-23 18:38 编辑 ]
 楼主| 发表于 2009-11-24 13:23:50 | 显示全部楼层
用TP_REACTOR自带的测试案例做了2个测试,1是测TP_REACTOR,按README中的介绍的方式跑了10分钟没发现问题,而README说的是他们在LINUX下大概1分钟就会发现问题。有可能是5.7已经解决了这个问题。第2个测试是测DEV_POLL_REACTOR,也跑了10分钟没发现问题,看来前面的推理是正确的,DEV_POLL_REACTOR支持线程安全。另外SELECT_REACTOR是没办法在多线程跑的,如果不在它的创建线程中运行RUN_EVENTS_LOOP,会直接返回。

这样EPOLL PROACTOR改起来就简单了,采用THREAD-SAFE INTERFACE模式,把会访问内部数据结构的所有方法都加上锁就可以了。
 楼主| 发表于 2009-11-24 13:50:08 | 显示全部楼层
再思考了下IOCP的实现和目前EPOLL PROACTOR实现的区别
1)IOCP的实现在性能上有一个优点,由于是内核支持的真正的异步IO,可以预先POST一个READ REQUEST,当SOCKET收到对方发来数据时,如果发现有PENDING REQUEST,不会把数据先存到SOCKET RECV BUFFER然后再传到APPLICATION BUFFER,而是直接传到APPLICATION BUFFER,也就是所谓的0 COPY,发送时也一样。而EPOLL触发EVENT READY时,数据已经先收在SOCKET RECV BUFFER,所以这时IOCP的性能好一些。
2)但是,这也是有代价的。预先POST 的 REQUEST所提供的APPLICATION BUFFER,会被系统锁住,被锁住的内存将不会被交换到SWAP中去。因此如果长期不COMPLETE,将长期占用宝贵的物理内存,因此系统对总的可以LOCK的内存量进行了限制。另外,还有一种更加宝贵的内核资源,NON-PAGED POLL。每一个POST的OVERLAPPED STRUCTURE,会占用0.5K,这就要求在系统中POST AIO REAQUEST要尽量节省。详细介绍请参见《WINDOWS 网络编程》。
《WINDOWS 网络编程》对第一个问题(LOCK MEMORY)提供了一个解决方案,就是POST一个0字节的REQUEST(仍然会占用0.5K的NON-PAGED POLL),这样当收到数据时,这个REQUEST就会COMPLETE,此时就可以进行同步IO。这其实就是SELECT/EPOLL的工作方式。第二个问题(NON-PAGED POLL耗尽)基本上很难解决,书上没提供解决方案,只是提醒在高并发高负载的时候要多注意节省,出现问题就自求多福吧。
3)所以,这种模式更适合于比较活跃,数据流量比较大的连接。如果一个连接对方长期不发数据,SERVER却长期LOCK内存,将是巨大的浪费。
4)由此可以想到对EPOLL的可能改进,提供一种0 COPY的模式,用户在WAIT的时候提供自己的BUFFER,当EPOLL READY的时候数据已经在用户的BUFFER中了而不必经过SOCKET BUFFER中转。也就是说是COMPLETE而不是READY。只有当用户BUFFER已满后才继续缓存到SOCKET BUFFER。
可以建议用户在处理相对活跃且数据流量比较大的连接时采取此模式以提高性能。LINUX相比WINDOWS有一点好处就是源代码开放,如果有确实需要的FEATURE,可以自己调。呵呵,等有时间看看EPOLL和SENDFILE的实现代码。
 楼主| 发表于 2009-11-24 13:56:17 | 显示全部楼层
EVENT的处理顺序我后来又想了下,EPOLLHUP | EPOLLERR的处理放在最前是比较好的,虽然性能提升并不明显,但是程序逻辑更加清晰了。我也是习惯于先处理异常情况再处理正常情况的逻辑。但是EPOLLIN和EPOLLOUT之间的顺序选择我有点拿不定,MODERN能说说把EPOLLOUT放在EPOLLIN前面是出于什么考虑么?
发表于 2009-11-24 16:44:02 | 显示全部楼层
wishel考虑问题还真是细致,
我如是说的依据是之前印象里看各种Reactor的代码,
基本上都是这么处理的,另外我也比较认同首先处理异常行为,

不过刚比较了各种Reactor的实现代码,看来我记得也有偏差。
毫无例外,各种主流版本的Reactor的实现都是按照写>异常>读的顺序进行处理的。
 楼主| 发表于 2009-11-25 15:42:13 | 显示全部楼层
那不必盲从他们。
如果没有说的过去的理由(如对性能的重大影响),我还是觉得程序逻辑清晰可读是第一位的。
 楼主| 发表于 2009-11-26 14:23:52 | 显示全部楼层
C++ NPV2 4.2
Years of experimentation and refinement resulted in the following order for event handler dispatching in the ACE_Select_Reactor::handle_events() method:
1.        Time-driven events
2.        Notifications
3.        Output I/O events
4.        Exception I/O events
5.        Input I/O events
Applications should generally not rely on the order in which the different types of events are dispatched since not all reactor implementations guarantee the same order. For example, the ACE_Dev_Poll_Reactor (page 114) might not dispatch notifications before I/O events.

认真领会了一下,个人理解:Time-driven events一般要求准时,放在最优先可以理解;Notifications一般也是比较紧急的通知,所以优先于一般io;out放在in之前应该是为了更快的给client反馈,也就是较高的responsiviness;Exception I/O events应该是指的带外数据,一般用不到。所以感觉err/hup->out->in比较合理。Timer和Notification目前Epll Proactor尚不支持。

另外,modern前面可能记错了,Dev_Poll_Reactor是支持Notification机制的,只是把notifications 和I/O events 放在了同等的优先级一同处理(我测试了下,epoll的通知机制好像是把pipe放在了inet socket之后)。
 楼主| 发表于 2009-11-26 14:26:09 | 显示全部楼层
多线程Epoll Proactor完成了。开了10个线程传了几个大文件简单测了下。
由于情况比较简单,没有用thread-safe interface pattern。简单说明一下:
1,互斥用ACE_Thread_Mutex,每个Handle一个。
2,由于epoll_wait(1, EPOLLONESHOT)可以保证不会有2个线程同时dispatch 同一handleevent,所以并发只会发生在start_asynch_*()dispatch之间。因此在start_asynch_*()中和epoll_wait(1)后的dispathch过程中加锁。锁的粒度是handle,所以不同handle的操作(startdispatch)可并行。
complete()之前释放一下锁,callback返回后再收回锁。之所以这样处理而不用递归锁(如ACE_TOKEN)的原因是为了避免死锁。
另外在提供给框架上层访问的public cacel_aio()中加了锁。
3,由于使用EPOLLONESHOT,所以dispatch过后要视情况resume。每个Handle保留一个resume flag

补充一下,Dev_Poll_Reactordemultiplex还是用的leader/follwer模式,唯一取得TOKEN的线程才可以运行epoll_wait,而没有利用epoll_wait(1, EPOLLONESHOT)
而且它的lock粒度是Reactor,所有handle的处理都用这一个。所以并发程度不高。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?用户注册

×
 楼主| 发表于 2009-11-26 15:05:30 | 显示全部楼层
Epoll Proactorepoll_wait(1, EPOLLONESHOT)ACE_WFMO_Reactor的语义非常相似,做一个对比:

1)
都是真正的concurrent demulitiplex,不是Leader/Follower模式,Wait不需要同步。而且每次都是只返回一个handleevent。性能要好于Leader/Follower

2)
Windowsapi WaitForMultipleObjects()同样是oneshot语义,系统set event后自动clear mask,如用户需要再次关注该event,需要explicitly reenable。(参见C++ NPV2 Sidebar 26: Why ACE_WFMO_Reactor Doesn't Suspend Handles

3)
对同一非并发handle(如tcp socket)的io需要进行同步。(udp socket为并发handle,不需要同步)

Leader/Follower模式是通过一个suspend/resume协议。(Leader/Follower模式的详细阐述可参见POSA2
目前Epoll Proactor的做法是利用EPOLLONESHOT,保证不会有多个线程同时被dispatch同一handle。同时对新加入的异步io请求进行控制,只有当handle为新产生(或刚回收再利用)时才直接注册到epoll,否则只更新相应的resume flag,等该handle对应的处理线程从handle_*返回时才通过resume的方式加入epoll
ACE_WFMO_Reactor的处理,书上要求是“explicitly protect race conditions”。可以利用上述Epoll Protactor的机制,也可以用其他的办法,书上的Logging_Event_Handler_WFMO例子是通过一个ACE_Thread_Mutex来进行serialization
4)
ACE_WFMO_ReactorWRITE_MASK是强制et语义,而Epoll Proactor用的是EPOLLONESHOT + resume,所以实质上是lt语义。

5)
ACE_WFMO_Reactor还有2个特别tricky的语义:deferred registration changedeferred handler clean upEpoll Proactor幸运的避免了这个麻烦的问题。

Epoll Proactor采用的resume flag机制,所以也有deferred registration change语义。但是proactor不支持用户显式 registration,所以这个语义不会影响用户。同时proactorhandler cleanup是全权交给用户处理的,框架不管。Epoll Proactor没有clean up的责任,自然没有deferred handler clean up语义。

[ 本帖最后由 wishel 于 2009-11-26 15:08 编辑 ]
 楼主| 发表于 2009-11-27 15:33:02 | 显示全部楼层
另外,由于epoll fd自身也是poll/epoll/selectable的(If  an epoll file descriptor has events waiting then it will indicate as being readable),所以像ACE_WIN32_Proactor一样,Epoll Proactor也可以和其他reactor(select,dev_poll等)整合在一起在一个线程里跑event loop。具体做法参见C++NPV2 8.5, Sidebar 58: Integrating Proactor and Reactor Events on Windows。
但是和ACE_WIN32_Proactor所不同的是,此时reactor所侦测到的event是ready event而不是complete event,所以将此event传给Epoll Proactor(间接继承自ACE_Event_Handler)后,Epoll Proactor需在handle_input()中调用handle_events_i进行进一步处理而不是调用complete。
这样做的好处是可以在一个线程里同时支持reactor和proactor语义,而不需考虑多线程同步(通常情况reactor和proactor必须在各自单独的线程中跑event loop)。
因此,即使是在windows下用整合reactor和proactor方式写的程序,仍然可以方便的移植到linux。不过如果reactor用的是wfmo的话,需要注意它和select的一些语义差别。
您需要登录后才可以回帖 登录 | 用户注册

本版积分规则

Archiver|手机版|小黑屋|ACE Developer ( 京ICP备06055248号 )

GMT+8, 2024-12-4 16:12 , Processed in 0.017832 second(s), 4 queries , Redis On.

Powered by Discuz! X3.5

© 2001-2023 Discuz! Team.

快速回复 返回顶部 返回列表