28 | I/O多路复用进阶:子线程使用poll处理连接I/O事件
该思维导图由 AI 生成,仅供参考
主 - 从 reactor 模式
- 深入了解
- 翻译
- 解释
- 总结
本文深入探讨了I/O多路复用进阶的主题,着重介绍了主-从reactor模式及其在网络编程中的应用。在高并发情况下,单一reactor线程可能无法有效处理连接建立和I/O事件分发的问题,因此提出了主-从reactor模式,通过将连接建立事件和I/O事件分离,并利用多个从reactor线程来提高I/O分发处理效率。此外,还介绍了主-从reactor+worker threads模式,将CPU密集型的工作从I/O线程中分离,交由worker线程池处理,以提高业务逻辑和I/O分发之间的耦合问题。通过深入讨论主-从reactor模式及其应用,为读者提供了在高性能网络程序框架中解决I/O分发效率和业务逻辑耦合问题的思路和实践方法。文章还提供了相关的样例程序,展示了如何配置主、从反应堆线程,以及如何在onMessage方法中获取子线程来处理特定工作。整体而言,本文为读者提供了解决高性能网络程序中I/O分发效率和业务逻辑耦合问题的实用方法和思路。
《网络编程实战》,新⼈⾸单¥59
全部留言(25)
- 最新
- 精选
- 钱1:阻塞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 的核数来灵活设置。
作者回复: 总结的很到位,有点惊艳 😁
2019-11-2479 - ray老师您好, 如果在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、线程、事件驱动,这些概念和实现都是实实在在存在的,为了让我们写代码更加的简单和直接,将这些复杂的概念藏在后面,通过新的编程范式来达到这样的目的,是现代程序语言发展的必然。
2020-04-12211 - 马不停蹄学习 netty 的时候了解到 reactor 模式,netty 的 (单 、主从)reactor 可以灵活配置,老师讲的模式真的是和 netty 设计一样 ,这次学习算是真正搞明白了哈哈
作者回复: Java的封装是非常漂亮,倘若能理解原理,就会更加容易理解它的封装了。
2019-11-127 - 刘系老师,我试验了程序,发现有一个问题。 服务器程序启动后输出结果与文章中的不一样。 ./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,实现在子线程中添加连接套接字。
作者回复: 我怎么觉的你的结果是对的呢?有可能我文章中贴的信息不够全,造成了一定的误导。
2019-10-1725 - Simple life我觉得老师这里onMessage回调中使用线程池方式有误,这里解码,处理,编码是串行操作的,多线程并不能带来性能的提升,主线程还是会阻塞不释放的,我觉得最佳的做法是,解码交给线程池去做,然后返回,解码完成后注册进sub-reactor中再交由下一个业务处理,业务处理,编码同上,实现解耦充分利用多线程
作者回复: 非常同意,这里不是使用有误,只是作为一个例子,在线程里统一处理了解码、处理和编码。你的说法是对的。
2020-08-034 - 进击的巨人Netty的主从reactor分别对应bossGroup和workerGroup,workerGroup处理非accept的io事件,至于业务逻辑是否交给另外的线程池处理,可以理解为netty并没有支持,原因是因为业务逻辑都需要开发者自己自定义提供,但在这点上,netty通过ChannelHandler+pipline提供了io事件和业务逻辑分离的能力,需要开发者添加自定义ChannelHandler,实现io事件到业务逻辑处理的线程分离。
作者回复: 嗯,netty确实是这样设计的,很多东西最后都是殊途同归。
2020-11-1523 - 疯狂的石头看老师源码,channel,buffer各种对象,调来调去的,给我调懵了。
作者回复: 最后一个部分会讲这部分的设计,不要晕哈。
2020-05-062 - 绿箭侠event_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 ()
2020-03-061 - 李朝辉fd为7的套接字应该是socketpair()调用创建的主-从reactor套接字对中,从reactor线程写,主reactor线程读的套接字,作用的话,个人推测应该是从reactor线程中的连接套接字关闭了(即连接断开了),将这样的事件反馈给主reactor,以通知主reactor线程,我已经准备好接收下一个连接套接字?
作者回复: 接近真相了,后续章节会揭开答案。
2020-01-121 - 李朝辉4核cpu,主reactor要占掉一个,只有3个可以分配给从核心。 按照老师的说法,是因为主reactor的工作相对比较简单,所以占用内核的时间很少,所以将从reactor分配满,然后最大化对连接套接字的处理能力吗?
作者回复: 我其实没想这么多,一般而言,worker线程的个数保持和cpu核一致,是一个比较常见的做法,例如nginx。
2020-01-121