网络编程实战
盛延敏
前大众点评云平台首席架构师
44207 人已学习
新⼈⾸单¥59
登录后,你可以任选4讲全文学习
课程目录
已完结/共 40 讲
网络编程实战
15
15
1.0x
00:00/00:00
登录|注册

28 | I/O多路复用进阶:子线程使用poll处理连接I/O事件

将CPU密集型工作从I/O线程中拿走,交给worker线程池处理
可根据CPU核数设置从反应堆线程数量
从反应堆线程负责已连接套接字的I/O事件分发
主反应堆线程负责连接建立事件分发
第二道:尝试修改服务器端代码,使用线程或线程池处理decode-compute-encode部分
第一道:为什么main-thread加入了一个fd为7的套接字?
从Netty的实现上来看,也遵循了这一原则
主从reactor模式提高了客户端连接的处理能力
多个telnet客户端交互,主线程只负责新连接建立,每个客户端数据的收发由不同的子线程提供服务
使用poll作为事件分发方式
主、从反应堆线程配置
初始化TCP server,指定线程数目
初始化acceptor
主线程event_loop
主-从reactor+worker threads模式
主-从reactor模式
单reactor反应堆模式浪费CPU资源
单reactor线程忙不过来
前一讲介绍了reactor反应堆模式
思考题
总结
样例程序结果
样例程序
解决方案
问题
前提
网络编程实战第28讲:I/O多路复用进阶

该思维导图由 AI 生成,仅供参考

你好,我是盛延敏,这里是网络编程实战第 28 讲,欢迎回来。
在前面的第 27 讲中,我们引入了 reactor 反应堆模式,并且让 reactor 反应堆同时分发 Acceptor 上的连接建立事件和已建立连接的 I/O 事件。
我们仔细想想这种模式,在发起连接请求的客户端非常多的情况下,有一个地方是有问题的,那就是单 reactor 线程既分发连接建立,又分发已建立连接的 I/O,有点忙不过来,在实战中的表现可能就是客户端连接成功率偏低。
再者,新的硬件技术不断发展,多核多路 CPU 已经得到极大的应用,单 reactor 反应堆模式看着大把的 CPU 资源却不用,有点可惜。
这一讲我们就将 acceptor 上的连接建立事件和已建立连接的 I/O 事件分离,形成所谓的主 - 从 reactor 模式。

主 - 从 reactor 模式

下面的这张图描述了主 - 从 reactor 模式是如何工作的。
主 - 从这个模式的核心思想是,主反应堆线程只负责分发 Acceptor 连接建立,已连接套接字上的 I/O 事件交给 sub-reactor 负责分发。其中 sub-reactor 的数量,可以根据 CPU 的核数来灵活设置。
比如一个四核 CPU,我们可以设置 sub-reactor 为 4。相当于有 4 个身手不凡的反应堆线程同时在工作,这大大增强了 I/O 分发处理的效率。而且,同一个套接字事件分发只会出现在一个反应堆线程中,这会大大减少并发处理的锁开销。
确认放弃笔记?
放弃后所记笔记将不保留。
新功能上线,你的历史笔记已初始化为私密笔记,是否一键批量公开?
批量公开的笔记不会为你同步至部落
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
  • 深入了解
  • 翻译
    • 英语
    • 中文简体
    • 中文繁体
    • 法语
    • 德语
    • 日语
    • 韩语
    • 俄语
    • 西班牙语
    • 阿拉伯语
  • 解释
  • 总结

本文深入探讨了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-24
    79
  • 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-12
    2
    11
  • 马不停蹄
    学习 netty 的时候了解到 reactor 模式,netty 的 (单 、主从)reactor 可以灵活配置,老师讲的模式真的是和 netty 设计一样 ,这次学习算是真正搞明白了哈哈

    作者回复: Java的封装是非常漂亮,倘若能理解原理,就会更加容易理解它的封装了。

    2019-11-12
    7
  • 刘系
    老师,我试验了程序,发现有一个问题。 服务器程序启动后输出结果与文章中的不一样。 ./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-17
    2
    5
  • Simple life
    我觉得老师这里onMessage回调中使用线程池方式有误,这里解码,处理,编码是串行操作的,多线程并不能带来性能的提升,主线程还是会阻塞不释放的,我觉得最佳的做法是,解码交给线程池去做,然后返回,解码完成后注册进sub-reactor中再交由下一个业务处理,业务处理,编码同上,实现解耦充分利用多线程

    作者回复: 非常同意,这里不是使用有误,只是作为一个例子,在线程里统一处理了解码、处理和编码。你的说法是对的。

    2020-08-03
    4
  • 进击的巨人
    Netty的主从reactor分别对应bossGroup和workerGroup,workerGroup处理非accept的io事件,至于业务逻辑是否交给另外的线程池处理,可以理解为netty并没有支持,原因是因为业务逻辑都需要开发者自己自定义提供,但在这点上,netty通过ChannelHandler+pipline提供了io事件和业务逻辑分离的能力,需要开发者添加自定义ChannelHandler,实现io事件到业务逻辑处理的线程分离。

    作者回复: 嗯,netty确实是这样设计的,很多东西最后都是殊途同归。

    2020-11-15
    2
    3
  • 疯狂的石头
    看老师源码,channel,buffer各种对象,调来调去的,给我调懵了。

    作者回复: 最后一个部分会讲这部分的设计,不要晕哈。

    2020-05-06
    2
  • 绿箭侠
    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-06
    1
  • 李朝辉
    fd为7的套接字应该是socketpair()调用创建的主-从reactor套接字对中,从reactor线程写,主reactor线程读的套接字,作用的话,个人推测应该是从reactor线程中的连接套接字关闭了(即连接断开了),将这样的事件反馈给主reactor,以通知主reactor线程,我已经准备好接收下一个连接套接字?

    作者回复: 接近真相了,后续章节会揭开答案。

    2020-01-12
    1
  • 李朝辉
    4核cpu,主reactor要占掉一个,只有3个可以分配给从核心。 按照老师的说法,是因为主reactor的工作相对比较简单,所以占用内核的时间很少,所以将从reactor分配满,然后最大化对连接套接字的处理能力吗?

    作者回复: 我其实没想这么多,一般而言,worker线程的个数保持和cpu核一致,是一个比较常见的做法,例如nginx。

    2020-01-12
    1
收起评论
显示
设置
留言
25
收藏
沉浸
阅读
分享
手机端
快捷键
回顶部