极客时间已完结课程限时免费阅读

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

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

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

讲述:冯永吉

时长06:34大小6.02M

你好,我是盛延敏,这里是网络编程实战第 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 分发处理的效率。而且,同一个套接字事件分发只会出现在一个反应堆线程中,这会大大减少并发处理的锁开销。
我来解释一下这张图,我们的主反应堆线程一直在感知连接建立的事件,如果有连接成功建立,主反应堆线程通过 accept 方法获取已连接套接字,接下来会按照一定的算法选取一个从反应堆线程,并把已连接套接字加入到选择好的从反应堆线程中。
主反应堆线程唯一的工作,就是调用 accept 获取已连接套接字,以及将已连接套接字加入到从反应堆线程中。不过,这里还有一个小问题,主反应堆线程和从反应堆线程,是两个不同的线程,如何把已连接套接字加入到另外一个线程中呢?更令人沮丧的是,此时从反应堆线程或许处于事件分发的无限循环之中,在这种情况下应该怎么办呢?
我在这里先卖个关子,这是高性能网络程序框架要解决的问题。在实战篇里,我将为这些问题一一解开答案。

主 - 从 reactor+worker threads 模式

如果说主 - 从 reactor 模式解决了 I/O 分发的高效率问题,那么 work threads 就解决了业务逻辑和 I/O 分发之间的耦合问题。把这两个策略组装在一起,就是实战中普遍采用的模式。大名鼎鼎的 Netty,就是把这种模式发挥到极致的一种实现。不过要注意 Netty 里面提到的 worker 线程,其实就是我们这里说的从 reactor 线程,并不是处理具体业务逻辑的 worker 线程。
下面贴的一段代码就是常见的 Netty 初始化代码,这里 Boss Group 就是 acceptor 主反应堆,workerGroup 就是从反应堆。而处理业务逻辑的线程,通常都是通过使用 Netty 的程序开发者进行设计和定制,一般来说,业务逻辑线程需要从 workerGroup 线程中分离,以便支持更高的并发度。
public final class TelnetServer {
static final int PORT = Integer.parseInt(System.getProperty("port", SSL? "8992" : "8023"));
public static void main(String[] args) throws Exception {
//产生一个reactor线程,只负责accetpor的对应处理
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
//产生一个reactor线程,负责处理已连接套接字的I/O事件分发
EventLoopGroup workerGroup = new NioEventLoopGroup(1);
try {
//标准的Netty初始,通过serverbootstrap完成线程池、channel以及对应的handler设置,注意这里讲bossGroup和workerGroup作为参数设置
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.handler(new LoggingHandler(LogLevel.INFO))
.childHandler(new TelnetServerInitializer(sslCtx));
//开启两个reactor线程无限循环处理
b.bind(PORT).sync().channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
这张图解释了主 - 从反应堆下加上 worker 线程池的处理模式。
主 - 从反应堆跟上面介绍的做法是一样的。和上面不一样的是,这里将 decode、compute、encode 等 CPU 密集型的工作从 I/O 线程中拿走,这些工作交给 worker 线程池来处理,而且这些工作拆分成了一个个子任务进行。encode 之后完成的结果再由 sub-reactor 的 I/O 线程发送出去。

样例程序

#include <lib/acceptor.h>
#include "lib/common.h"
#include "lib/event_loop.h"
#include "lib/tcp_server.h"
char rot13_char(char c) {
if ((c >= 'a' && c <= 'm') || (c >= 'A' && c <= 'M'))
return c + 13;
else if ((c >= 'n' && c <= 'z') || (c >= 'N' && c <= 'Z'))
return c - 13;
else
return c;
}
//连接建立之后的callback
int onConnectionCompleted(struct tcp_connection *tcpConnection) {
printf("connection completed\n");
return 0;
}
//数据读到buffer之后的callback
int onMessage(struct buffer *input, struct tcp_connection *tcpConnection) {
printf("get message from tcp connection %s\n", tcpConnection->name);
printf("%s", input->data);
struct buffer *output = buffer_new();
int size = buffer_readable_size(input);
for (int i = 0; i < size; i++) {
buffer_append_char(output, rot13_char(buffer_read_char(input)));
}
tcp_connection_send_buffer(tcpConnection, output);
return 0;
}
//数据通过buffer写完之后的callback
int onWriteCompleted(struct tcp_connection *tcpConnection) {
printf("write completed\n");
return 0;
}
//连接关闭之后的callback
int onConnectionClosed(struct tcp_connection *tcpConnection) {
printf("connection closed\n");
return 0;
}
int main(int c, char **v) {
//主线程event_loop
struct event_loop *eventLoop = event_loop_init();
//初始化acceptor
struct acceptor *acceptor = acceptor_init(SERV_PORT);
//初始tcp_server,可以指定线程数目,这里线程是4,说明是一个acceptor线程,4个I/O线程,没一个I/O线程
//tcp_server自己带一个event_loop
struct TCPserver *tcpServer = tcp_server_init(eventLoop, acceptor, onConnectionCompleted, onMessage,
onWriteCompleted, onConnectionClosed, 4);
tcp_server_start(tcpServer);
// main thread for acceptor
event_loop_run(eventLoop);
}
我们的样例程序几乎和第 27 讲的一样,唯一的不同是在创建 TCPServer 时,线程的数量设置不再是 0,而是 4。这里线程是 4,说明是一个主 acceptor 线程,4 个从 reactor 线程,每一个线程都跟一个 event_loop 一一绑定。
你可能会问,这么简单就完成了主、从线程的配置?
答案是 YES。这其实是设计框架需要考虑的地方,一个框架不仅要考虑性能、扩展性,也需要考虑可用性。可用性部分就是程序开发者如何使用框架。如果我是一个开发者,我肯定关心框架的使用方式是不是足够方便,配置是不是足够灵活等。
像这里,可以根据需求灵活地配置主、从反应堆线程,就是一个易用性的体现。当然,因为时间有限,我没有考虑 woker 线程的部分,这部分其实应该是应用程序自己来设计考虑。网络编程框架通过回调函数暴露了交互的接口,这里应用程序开发者完全可以在 onMessage 方法里面获取一个子线程来处理 encode、compute 和 encode 的工作,像下面的示范代码一样。
//数据读到buffer之后的callback
int onMessage(struct buffer *input, struct tcp_connection *tcpConnection) {
printf("get message from tcp connection %s\n", tcpConnection->name);
printf("%s", input->data);
//取出一个线程来负责decode、compute和encode
struct buffer *output = thread_handle(input);
//处理完之后再通过reactor I/O线程发送数据
tcp_connection_send_buffer(tcpConnection, output);
return

样例程序结果

我们启动这个服务器端程序,你可以从服务器端的输出上看到使用了 poll 作为事件分发方式。
多打开几个 telnet 客户端交互,main-thread 只负责新的连接建立,每个客户端数据的收发由不同的子线程 Thread-1、Thread-2、Thread-3 和 Thread-4 来提供服务。
这里由于使用了子线程进行 I/O 处理,主线程可以专注于新连接处理,从而大大提高了客户端连接成功率。
$./poll-server-multithreads
[msg] set poll as dispatcher
[msg] add channel fd == 4, main thread
[msg] poll added channel fd==4
[msg] set poll as dispatcher
[msg] add channel fd == 7, main thread
[msg] poll added channel fd==7
[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
[msg] add channel fd == 9, main thread
[msg] poll added channel fd==9
[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
[msg] add channel fd == 11, main thread
[msg] poll added channel fd==11
[msg] event loop thread init and signal, Thread-3
[msg] event loop thread started, Thread-3
[msg] set poll as dispatcher
[msg] event loop run, Thread-3
[msg] add channel fd == 13, main thread
[msg] poll added channel fd==13
[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 == 5, main thread
[msg] poll added channel fd==5
[msg] event loop run, main thread
[msg] get message channel i==1, fd==5
[msg] activate channel fd == 5, revents=2, main thread
[msg] new connection established, socket == 14
connection completed
[msg] get message channel i==0, fd==7
[msg] activate channel fd == 7, revents=2, Thread-1
[msg] wakeup, Thread-1
[msg] add channel fd == 14, Thread-1
[msg] poll added channel fd==14
[msg] get message channel i==1, fd==14
[msg] activate channel fd == 14, revents=2, Thread-1
get message from tcp connection connection-14
fasfas
[msg] get message channel i==1, fd==14
[msg] activate channel fd == 14, revents=2, Thread-1
get message from tcp connection connection-14
fasfas
asfa
[msg] get message channel i==1, fd==5
[msg] activate channel fd == 5, revents=2, main thread
[msg] new connection established, socket == 15
connection completed
[msg] get message channel i==0, fd==9
[msg] activate channel fd == 9, revents=2, Thread-2
[msg] wakeup, Thread-2
[msg] add channel fd == 15, Thread-2
[msg] poll added channel fd==15
[msg] get message channel i==1, fd==15
[msg] activate channel fd == 15, revents=2, Thread-2
get message from tcp connection connection-15
afasdfasf
[msg] get message channel i==1, fd==15
[msg] activate channel fd == 15, revents=2, Thread-2
get message from tcp connection connection-15
afasdfasf
safsafa
[msg] get message channel i==1, fd==15
[msg] activate channel fd == 15, revents=2, Thread-2
[msg] poll delete channel fd==15
connection closed
[msg] get message channel i==1, fd==5
[msg] activate channel fd == 5, revents=2, main thread
[msg] new connection established, socket == 16
connection completed
[msg] get message channel i==0, fd==11
[msg] activate channel fd == 11, revents=2, Thread-3
[msg] wakeup, Thread-3
[msg] add channel fd == 16, Thread-3
[msg] poll added channel fd==16
[msg] get message channel i==1, fd==16
[msg] activate channel fd == 16, revents=2, Thread-3
get message from tcp connection connection-16
fdasfasdf
[msg] get message channel i==1, fd==14
[msg] activate channel fd == 14, revents=2, Thread-1
[msg] poll delete channel fd==14
connection closed
[msg] get message channel i==1, fd==5
[msg] activate channel fd == 5, revents=2, main thread
[msg] new connection established, socket == 17
connection completed
[msg] get message channel i==0, fd==13
[msg] activate channel fd == 13, revents=2, Thread-4
[msg] wakeup, Thread-4
[msg] add channel fd == 17, Thread-4
[msg] poll added channel fd==17
[msg] get message channel i==1, fd==17
[msg] activate channel fd == 17, revents=2, Thread-4
get message from tcp connection connection-17
qreqwrq
[msg] get message channel i==1, fd==16
[msg] activate channel fd == 16, revents=2, Thread-3
[msg] poll delete channel fd==16
connection closed
[msg] get message channel i==1, fd==5
[msg] activate channel fd == 5, revents=2, main thread
[msg] new connection established, socket == 18
connection completed
[msg] get message channel i==0, fd==7
[msg] activate channel fd == 7, revents=2, Thread-1
[msg] wakeup, Thread-1
[msg] add channel fd == 18, Thread-1
[msg] poll added channel fd==18
[msg] get message channel i==1, fd==18
[msg] activate channel fd == 18, revents=2, Thread-1
get message from tcp connection connection-18
fasgasdg
^C

总结

本讲主要讲述了主从 reactor 模式,主从 reactor 模式中,主 reactor 只负责连接建立的处理,而把已连接套接字的 I/O 事件分发交给从 reactor 线程处理,这大大提高了客户端连接的处理能力。从 Netty 的实现上来看,也遵循了这一原则。

思考题

和往常一样,给你留两道思考题:
第一道,从日志输出中,你还可以看到 main-thread 首先加入了 fd 为 4 的套接字,这个是监听套接字,很好理解。可是这里的 main-thread 又加入了一个 fd 为 7 的套接字,这个套接字是干什么用的呢?
第二道,你可以试着修改一下服务器端的代码,把 decode-compute-encode 部分使用线程或者线程池来处理。
欢迎你在评论区写下你的思考,或者在 GitHub 上上传修改过的代码,我会和你一起交流,也欢迎把这篇文章分享给你的朋友或者同事,一起交流一下。
分享给需要的人,Ta购买本课程,你将得18
生成海报并分享

赞 7

提建议

上一篇
27 | I/O多路复用遇上线程:使用poll单线程处理所有I/O事件
下一篇
29 | 渐入佳境:使用epoll和多线程模型
unpreview
 写留言

精选留言(25)

  • 2019-11-24
    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 的核数来灵活设置。
    展开

    作者回复: 总结的很到位,有点惊艳 😁

    74
  • ray
    2020-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 life
    2020-08-03
    我觉得老师这里onMessage回调中使用线程池方式有误,这里解码,处理,编码是串行操作的,多线程并不能带来性能的提升,主线程还是会阻塞不释放的,我觉得最佳的做法是,解码交给线程池去做,然后返回,解码完成后注册进sub-reactor中再交由下一个业务处理,业务处理,编码同上,实现解耦充分利用多线程

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

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

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

    共 2 条评论
    2
  • 疯狂的石头
    2020-05-06
    看老师源码,channel,buffer各种对象,调来调去的,给我调懵了。

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

    2
  • 绿箭侠
    2020-03-06
    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 ()

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

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

    1
  • 李朝辉
    2020-01-12
    4核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,这两个意思接近,一个是从设计模式角度,另一个是从程序设计功能角度。

  • Morton
    2021-01-25
    老师,Reactor线程池占用了一部分cpu核,然后worker线程如果用线程池又会占用一部分cpu核,假设8核机器应该怎么分配线程池?reactor占4个worker线程占4个?

    作者回复: 你说的是一个分法,主要还是看你worker线程干活的多少,最好还是经过实际压测的结果来决定cpu分配比。

  • 木子皿
    2020-10-19
    终于把整个代码流程走通了,太不容易了,不过还只是看得懂,写出来还是很难!

    作者回复: 先看懂,再试着改

  • 木子皿
    2020-10-19
    坚持坚持,无数次想放弃!快要结束了!

    作者回复: 坚持就是胜利 ✌️

  • 我的名字不叫1988
    2020-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

    作者回复: 嗯,我看到了,改起来~

  • jhren
    2020-05-30
    请问老师,我看见有人在发送端使用httpcomponents I/O reactor,请问合理吗? https://hc.apache.org/httpcomponents-core-ga/tutorial/html/nio.html#d5e477

    作者回复: 合理啊,客户端也可以事件驱动。

  • supermouse
    2020-02-25
    老师,请问能不能说一下上一讲和这一讲的代码中的channel是干什么用的?一直没看明白

    作者回复: channel是一个抽象,表示一个连接通道,有可能是tcp连接,也有可能是内部的一个实现(如sockertpair),你可以把它和connection做一个有效关联。