28 | I/O多路复用进阶:子线程使用poll处理连接I/O事件
28 | I/O多路复用进阶:子线程使用poll处理连接I/O事件
讲述:冯永吉
时长06:34大小6.02M
主 - 从 reactor 模式
主 - 从 reactor+worker threads 模式
样例程序
样例程序结果
总结
思考题
赞 7
提建议
精选留言(25)
- 钱2019-11-241:阻塞IO+多进程——实现简单,性能一般 2:阻塞IO+多线程——相比于阻塞IO+多进程,减少了上下文切换所带来的开销,性能有所提高。 3:阻塞IO+线程池——相比于阻塞IO+多线程,减少了线程频繁创建和销毁的开销,性能有了进一步的提高。 4:Reactor+线程池——相比于阻塞IO+线程池,采用了更加先进的事件驱动设计思想,资源占用少、效率高、扩展性强,是支持高性能高并发场景的利器。 5:主从Reactor+线程池——相比于Reactor+线程池,将连接建立事件和已建立连接的各种IO事件分离,主Reactor只负责处理连接事件,从Reactor只负责处理各种IO事件,这样能增加客户端连接的成功率,并且可以充分利用现在多CPU的资源特性进一步的提高IO事件的处理效率。 6:主 - 从Reactor模式的核心思想是,主Reactor线程只负责分发 Acceptor 连接建立,已连接套接字上的 I/O 事件交给 从Reactor 负责分发。其中 sub-reactor 的数量,可以根据 CPU 的核数来灵活设置。展开
作者回复: 总结的很到位,有点惊艳 😁
74 - ray2020-04-12老师您好, 如果在worker thread pool里面的thread在执行工作时,又遇到了I/O。是不是也可以在worker thread pool里面加入epoll来轮询?但通常在worker thread里面遇到的I/O应该都已经不是network I/O了,而是sql、读写file、或是向第三方发起api,我不是很确定能否用epoll来处理。 有在google上查到,worker thread或worker process若遇到I/O,似乎会用一种叫作coroutine的方式来切换cpu的使用权。此种切换方式,不涉及kernel,全是在应用程序做切换。 这边想请教老师,对在worker thread里面遇到I/O问题时的处理方式或是心得是什么? 谢谢老师的分享!展开
作者回复: 正如你所说,一般我们这里说的worker都是正经干苦力活的,如encode/decode,业务逻辑等,在网络编程范式下,我们不推荐I/O操作又混在worker线程里面。 而你提的routine的方式,应该是一种I/O处理的编程方式,当我们使用这样routine的时候,如果有I/O操作,对应的cpu资源被切换回去,实际上又回到了I/O事件驱动的范式。这里的routine本身是被语言自己所封装的I/O事件驱动机制所包装的,你可以认为在这种情况下,语言(如C++/Golang)实现了内生的事件驱动机制,让我们可以直接关注之前的encode/decode和业务逻辑的编码。 不管技术怎么变化,cpu、线程、事件驱动,这些概念和实现都是实实在在存在的,为了让我们写代码更加的简单和直接,将这些复杂的概念藏在后面,通过新的编程范式来达到这样的目的,是现代程序语言发展的必然。
11 - 马不停蹄2019-11-12学习 netty 的时候了解到 reactor 模式,netty 的 (单 、主从)reactor 可以灵活配置,老师讲的模式真的是和 netty 设计一样 ,这次学习算是真正搞明白了哈哈
作者回复: Java的封装是非常漂亮,倘若能理解原理,就会更加容易理解它的封装了。
5 - 刘系2019-10-17老师,我试验了程序,发现有一个问题。 服务器程序启动后输出结果与文章中的不一样。 ./poll-server-multithreads [msg] set poll as dispatcher, main thread [msg] add channel fd == 4, main thread [msg] poll added channel fd==4, main thread [msg] set poll as dispatcher, Thread-1 [msg] add channel fd == 8, Thread-1 [msg] poll added channel fd==8, Thread-1 [msg] event loop thread init and signal, Thread-1 [msg] event loop run, Thread-1 [msg] event loop thread started, Thread-1 [msg] set poll as dispatcher, Thread-2 [msg] add channel fd == 10, Thread-2 [msg] poll added channel fd==10, Thread-2 [msg] event loop thread init and signal, Thread-2 [msg] event loop run, Thread-2 [msg] event loop thread started, Thread-2 [msg] set poll as dispatcher, Thread-3 [msg] add channel fd == 19, Thread-3 [msg] poll added channel fd==19, Thread-3 [msg] event loop thread init and signal, Thread-3 [msg] event loop run, Thread-3 [msg] event loop thread started, Thread-3 [msg] set poll as dispatcher, Thread-4 [msg] add channel fd == 21, Thread-4 [msg] poll added channel fd==21, Thread-4 [msg] event loop thread init and signal, Thread-4 [msg] event loop run, Thread-4 [msg] event loop thread started, Thread-4 [msg] add channel fd == 6, main thread [msg] poll added channel fd==6, main thread [msg] event loop run, main thread 各个子线程启动后创建的套接字对是添加在子线程的eventloop上的,而不是像文章中的全是添加在主线程中。 从我阅读代码来看,确实也是添加在子线程中。不知道哪里不对? 主线程给子线程下发连接套接字是通过主线程调用event_loop_add_channel_event完成的,当主线程中发现eventloop和自己不是同一个线程,就通过给这个evenloop的套接字对发送一个“a”产生事件唤醒,然后子线程处理pending_channel,实现在子线程中添加连接套接字。展开
作者回复: 我怎么觉的你的结果是对的呢?有可能我文章中贴的信息不够全,造成了一定的误导。
共 2 条评论5 - Simple life2020-08-03我觉得老师这里onMessage回调中使用线程池方式有误,这里解码,处理,编码是串行操作的,多线程并不能带来性能的提升,主线程还是会阻塞不释放的,我觉得最佳的做法是,解码交给线程池去做,然后返回,解码完成后注册进sub-reactor中再交由下一个业务处理,业务处理,编码同上,实现解耦充分利用多线程
作者回复: 非常同意,这里不是使用有误,只是作为一个例子,在线程里统一处理了解码、处理和编码。你的说法是对的。
4 - 进击的巨人2020-11-15Netty的主从reactor分别对应bossGroup和workerGroup,workerGroup处理非accept的io事件,至于业务逻辑是否交给另外的线程池处理,可以理解为netty并没有支持,原因是因为业务逻辑都需要开发者自己自定义提供,但在这点上,netty通过ChannelHandler+pipline提供了io事件和业务逻辑分离的能力,需要开发者添加自定义ChannelHandler,实现io事件到业务逻辑处理的线程分离。
作者回复: 嗯,netty确实是这样设计的,很多东西最后都是殊途同归。
共 2 条评论2 - 疯狂的石头2020-05-06看老师源码,channel,buffer各种对象,调来调去的,给我调懵了。
作者回复: 最后一个部分会讲这部分的设计,不要晕哈。
2 - 绿箭侠2020-03-06event_loop.c --- struct event_loop *event_loop_init_with_name(char *thread_name): #ifdef EPOLL_ENABLE yolanda_msgx("set epoll as dispatcher, %s", eventLoop->thread_name); eventLoop->eventDispatcher = &epoll_dispatcher; #else yolanda_msgx("set poll as dispatcher, %s", eventLoop->thread_name); eventLoop->eventDispatcher = &poll_dispatcher; #endif eventLoop->event_dispatcher_data = eventLoop->eventDispatcher->init(eventLoop); 没找到 EPOLL_ENABLE 的定义,老师怎么考虑的!!这里的话是否只能在event_loop.h 所包含的头文件中去找定义?展开
作者回复: 这个是通过CMake来定义的,通过CMake的check来检验是否enable epoll,这个宏出现在动态生成的头文件中。 # check epoll and add config.h for the macro compilation include(CheckSymbolExists) check_symbol_exists(epoll_create "sys/epoll.h" EPOLL_EXISTS) if (EPOLL_EXISTS) # Linux下设置为epoll set(EPOLL_ENABLE 1 CACHE INTERNAL "enable epoll") # Linux下也设置为poll # set(EPOLL_ENABLE "" CACHE INTERNAL "not enable epoll") else () set(EPOLL_ENABLE "" CACHE INTERNAL "not enable epoll") endif ()
1 - 李朝辉2020-01-12fd为7的套接字应该是socketpair()调用创建的主-从reactor套接字对中,从reactor线程写,主reactor线程读的套接字,作用的话,个人推测应该是从reactor线程中的连接套接字关闭了(即连接断开了),将这样的事件反馈给主reactor,以通知主reactor线程,我已经准备好接收下一个连接套接字?
作者回复: 接近真相了,后续章节会揭开答案。
1 - 李朝辉2020-01-124核cpu,主reactor要占掉一个,只有3个可以分配给从核心。 按照老师的说法,是因为主reactor的工作相对比较简单,所以占用内核的时间很少,所以将从reactor分配满,然后最大化对连接套接字的处理能力吗?
作者回复: 我其实没想这么多,一般而言,worker线程的个数保持和cpu核一致,是一个比较常见的做法,例如nginx。
1 - 川云2019-10-11可不可以把调用poll代码的位置展示一下
作者回复: 调用poll的代码已经封装在框架中,具体可以看 https://github.com/froghui/yolanda lib/event_dispatcher.h lib/poll_dispatcher.h lib/poll_dispatcher.c
1 - 王蓬勃2021-12-20老师 请问那个event_loop_do_channel_event函数什么时候才进入不是同一个线程的判断中去?看不明白了
作者回复: 这段判断,判断的依据是当前的线程id是event_loop里的记录,是不是一致的。 if (!isInSameThread(eventLoop)) { event_loop_wakeup(eventLoop); } else { event_loop_handle_pending_channel(eventLoop); }
- 这一行,30年2021-11-09#include <lib/acceptor.h> #include "lib/common.h" #include "lib/event_loop.h" #include "lib/tcp_server.h" 把老师的代码copy过去,这些类库都报错,不用老师引用的宏用什么宏?展开
作者回复: 你是用什么方法编译的?我的工程统一用CMake编译,应该没问题。 另外,这里的库都在源代码的lib目录下。
- 企鹅2021-09-17老师,主reactor只分发acceptor上建立连接的事件,不应该是client->acceptor -> master reactor么,图上是client->master reactor->acceptor这里看晕了
作者回复: master reactor 反应堆线程,就是主acceptor,这两个意思接近,一个是从设计模式角度,另一个是从程序设计功能角度。
- Morton2021-01-25老师,Reactor线程池占用了一部分cpu核,然后worker线程如果用线程池又会占用一部分cpu核,假设8核机器应该怎么分配线程池?reactor占4个worker线程占4个?
作者回复: 你说的是一个分法,主要还是看你worker线程干活的多少,最好还是经过实际压测的结果来决定cpu分配比。
- 木子皿2020-10-19终于把整个代码流程走通了,太不容易了,不过还只是看得懂,写出来还是很难!
作者回复: 先看懂,再试着改
- 木子皿2020-10-19坚持坚持,无数次想放弃!快要结束了!
作者回复: 坚持就是胜利 ✌️
- 我的名字不叫19882020-07-14老师,github上面的源码,lib/poll_dispacher.c文件里面的poll_add、poll_del、poll_update等函数里面的“if (i > INIT_POLL_SIZE)” 判断有问题,因为 for 循环结束之后,i 的可能的最大值为INIT_POLL_SIZE,所以永远不可能大于INIT_POLL_SIZE
作者回复: 嗯,我看到了,改起来~
- jhren2020-05-30请问老师,我看见有人在发送端使用httpcomponents I/O reactor,请问合理吗? https://hc.apache.org/httpcomponents-core-ga/tutorial/html/nio.html#d5e477
作者回复: 合理啊,客户端也可以事件驱动。
- supermouse2020-02-25老师,请问能不能说一下上一讲和这一讲的代码中的channel是干什么用的?一直没看明白
作者回复: channel是一个抽象,表示一个连接通道,有可能是tcp连接,也有可能是内部的一个实现(如sockertpair),你可以把它和connection做一个有效关联。