33 | 自己动手写高性能HTTP服务器(二):I/O模型和多线程模型实现
33 | 自己动手写高性能HTTP服务器(二):I/O模型和多线程模型实现
讲述:冯永吉
时长11:35大小10.62M
多线程设计的几个考虑
主线程等待多个 sub-reactor 子线程初始化完
增加已连接套接字事件到 sub-reactor 线程中
总结
思考题
赞 2
提建议
精选留言(24)
- 酸葡萄2019-12-11老师,你好,有个地方不是很明白, 为什么event_loop_channel_buffer_nolock(eventLoop, fd, channel1, type);是往子线程的数据中增加需要处理的 channel event 对象呢? void event_loop_channel_buffer_nolock(struct event_loop *eventLoop, int fd, struct channel *channel1, int type) { //add channel into the pending list struct channel_element *channelElement = malloc(sizeof(struct channel_element)); channelElement->channel = channel1; channelElement->type = type;//1 add (1: add 2: delete) channelElement->next = NULL; //第一个元素 channel_element是channel的链表, // eventLoop pending_head和pending_tail维护的是channelElement的链表 //这样的话最终还是event_loop包含了channel(event_loop->channelElement->channel) if (eventLoop->pending_head == NULL) { eventLoop->pending_head = eventLoop->pending_tail = channelElement; } else { eventLoop->pending_tail->next = channelElement; eventLoop->pending_tail = channelElement; } } void *event_loop_thread_run(void *arg) { struct event_loop_thread *eventLoopThread = (struct event_loop_thread *) arg; pthread_mutex_lock(&eventLoopThread->mutex); // 初始化化event loop,之后通知主线程 eventLoopThread->eventLoop = event_loop_init_with_name(eventLoopThread->thread_name); yolanda_msgx("event loop thread init and signal, %s", eventLoopThread->thread_name); pthread_cond_signal(&eventLoopThread->cond); pthread_mutex_unlock(&eventLoopThread->mutex); //子线程event loop run event_loop_run(eventLoopThread->eventLoop); } struct event_loop_thread { struct event_loop *eventLoop;//主线程和子线程共享 pthread_t thread_tid; /* thread ID */ pthread_mutex_t mutex; pthread_cond_t cond; char * thread_name; long thread_count; /* # connections handled */ }; event_loop_channel_buffer_nolock这个函数中是往eventLoop的链表中注册事件,可是这里的eventLoop是和子线程处理函数 event_loop_thread_run中eventLoopThread->eventLoop不是一个eventLoop啊,这个eventLoopThread->eventLoop不才是主子线程共享的吗?展开
作者回复: 我们还是用acceptor线程和I/O线程这样来区分比较好。 acceptor线程在发现有连接到达后,通过调用event_loop_channel_buffer_nolock函数,往I/O线程的eventLoop里面增加了新的套接字,也就是你说的注册链表。 这里的关键是每个线程都是一个独立的eventLoop,acceptor有自己的eventLoop,I/O线程有自己的eventLoop。没有主子线程共享eventLoop,一个eventLoop就对应一个线程。
5 - YUAN2020-12-11为什么不直接让子线程自己调用accept而要主线程调用呢?
作者回复: 那主线程干啥呢?
4 - 时间2020-09-23线程池个数有限,如何处理成千上万的链接?假如线程池共四个线程,正在处理四个链接。再来一个链接如何处理呢?
作者回复: 好问题。 线程不是每时每刻都要干活的,就好比一个流水线工人,只有轮到他的时候,他才需要出力干活。如果这四个连接都在干活,那第五个只好等任意一个线程空闲出来。 所有的事情,秘诀都在于"分时复用",比如你的cpu,也就4个core,为啥同时可以打游戏,写文稿,看电影,跑程序.....想通了这个,就想通了如何处理多个连接了。
共 3 条评论4 - keepgoing2020-08-24想问问老师关于基础语法的问题,代码里很多地方对象都是相互引用的,比如tcp_connection里引用了channel指针, channel 对象里引用了tcp_connection指针, dispatcher里引用了event_loop指针, event_loop里也引用了dispatcher指针。这样代码编译的时候为什么不会引起报错。。
作者回复: 为什么会报错呢?每个指针就是一个内存地址。
共 2 条评论1 - 郑祖煌2020-07-15第一道, 可以直接在应用层上将输入的线程个数*2 。 第二道,(1)可以判断已经创建好的线程 那个线程的事件个数最少,挂在事件最少的那个线程上。
作者回复: 👍
1 - MoonGod2019-10-23老师关于加锁这里有个疑问,如果加锁的目的是让主线程等待子线程初始化event loop。那不加锁不是也可以达到这个目的吗?主线程while 循环里面不断判断子线程的event loop是否不为null不就可以了?为啥一定要加一把锁呢?
作者回复: 好问题, 我答疑统一回答吧。
共 2 条评论1 - 鱼向北游2019-10-23netty选子线程是两种算法,都是有个原子自增计数,如果线程数不是2的幂用取模,如果是就是按位与线程数减一
作者回复: 嗯,涨知识了,代码贴一个?
共 3 条评论1 - 菜鸡互啄2022-09-07 来自上海老师你好 最近在用C++重写示例代码。我发现个问题。在C代码中 不同线程同时在poll同一份event_set。如果poll函数时间参数写成-1。多启几个终端执行nc指令。经常会出现没有callback的现象。如果poll不是同一份event_set 就没有这个问题。网上也有别人的一些讨论。对此老师怎么看。望回复。https://stackoverflow.com/questions/18891500/multiple-threads-doing-poll-or-select-on-a-single-socket-or-pipe
- Running man2022-09-01 来自浙江老师您好,子线程channel以及channel_element对象都是动态分配的内存,但在连接close后并未看到释放,是否是内存泄露了?
- 雨里2022-02-28没有看明白主从reactor这个主线程是如何唤醒子线程的??? 1、就单reactor而言,主线程创建管道fd,正常来说应该是在epoll_wait上注册0端读事件,往管道1端写数据的方式来唤醒epoll。 2、而主从reactor代码来看,主线程和子线程都创建了一对pairfd,主线程的管道1端注册在主线程的epoll上,这样即使往管道中写数据,也只是唤醒主线程,怎么会唤醒子线程呢??,代码中好像没有将主线程的管道fd一端注册在子线程的epoll上。是不是下面的这行代码导致的 eventLoop->eventDispatcher = &poll_dispatcher; 主线程和子线程共用一个同一个poll_dispatcher对象,还是没有看出在哪个地方传递的fd??展开
作者回复: 主线程,可以往子线程的管道上写数据,从而唤醒子线程。 传递fd的代码在这个函数里。 int event_loop_do_channel_event(struct event_loop *eventLoop, int fd, struct channel *channel1, int type) { //get the lock pthread_mutex_lock(&eventLoop->mutex); assert(eventLoop->is_handle_pending == 0); event_loop_channel_buffer_nolock(eventLoop, fd, channel1, type); //release the lock pthread_mutex_unlock(&eventLoop->mutex); if (!isInSameThread(eventLoop)) { event_loop_wakeup(eventLoop); } else { event_loop_handle_pending_channel(eventLoop); } return 0; }
共 2 条评论 - zssdhr2022-02-20老师,关于 event_loop_thread 有两个问题。 1. 为什么主线程要等待子线程初始化完成?是担心 tcp_server_init 后、但子线程还未初始化完成时,thread_pool_get_loop 无法找到子线程来处理新来的连接吗? 2. 文中提到”你可能会问,主线程是循环在等待每个子线程完成初始化,如果进入第二个循环,等待第二个子线程完成初始化,而此时第二个子线程已经初始化完成了,该怎么办?“ 主线程不是等第一个子线程初始化完成后才会进入下一个循环启动第二个子线程吗?怎么会出现”而此时第二个子线程已经初始化完成了“?展开
作者回复: 1.可以这么认为; 2.你说的没错,是按照顺序的,第一个完成,再第二个,问题是到第二个以后,如何判断第二个已经完成初始化了,因为这里的线程都是异步的,所以有可能线程初始化完成了,才进入判断。
- Janus Pen2022-01-10老师:event_loop_do_channel_event函数中的event_loop_handle_pending_channel函数调用与event_loop_run函数中的event_loop_handle_pending_channel函数调用是否重复?
作者回复: 不会。 两个的用处不一样,其中,event_loop_run中,是为了处理所有待添加的事件,这个肯定是在dispatch线程中执行的。 而event_loop_do_channel_event,是既可能在dispatch线程,也可能不在,而在dispatch线程中,每次调用event_loop_handle_pending_channel完成事件的实时添加,如果不是,就唤醒dispatch线程,让它自己完成添加。
- 消失的时光2022-01-07老师你好,不是很理解为什么要socketpair唤醒,直接把新连接的socket加到epoll里面,有发送就的数据过来,这个线程自己不会醒吗?
作者回复: epoll不允许这么干。
共 3 条评论1 - 菜鸡互啄2022-01-05老师你好 关于第二点 是不是相当于没有需要遍历的描述符 导致一直卡在poll或者select上。所以手动构造socketpair作为初始描述符。再添加真正新的描述符时 用socketpair把程序从poll或者select阻塞上解放出来 以获取达到添加描述符的时机?我的理解对吗?
作者回复: 是的。
- 群书2021-11-12老师你好,逻辑线程写数据到发送队列,同时通知唤醒io线程,这个通知方式目前比较常规的做法是套接字对或者事件fd 实际测试下来 都会增加主线程的系统调用 有什么优化办法呢
作者回复: 常规做法是这样的,我还想不出超常规做法😢。 这个不会增加系统负担,不用担心。连Netty都是这么干的。
- YUAN2020-12-04主线程和丛线程不是共享内存吗?为什么还要socketpair唤醒呢?
作者回复: 唤醒的目的是让主线程那个负责分发的家伙醒来干活,不要继续等待了。
- 忆水寒2020-09-06老师,main-EventLoop和sub-EventLoop里面的eventLoop->eventDispatcher = &epoll_dispatcher;都是指向一个epoll_dispatcher。其中main-EventLoop用于accept新连接,获得新连接封装channel交给某一个sub-EventLoop去处理。假如dispatch有事件,是不是子线程也会从dispatch处惊醒,这是不是有“惊群效应”吗?
作者回复: 我认为不会。原因是每个eventloop对应一个独立的dispatch。虽然公用的是一个epoll_dispatcher,但是你可以注意到在调用epoll_wait函数时,是取自各自event_loop里的独立数据。 epoll_dispatcher_data *epollDispatcherData = (epoll_dispatcher_data *) eventLoop->event_dispatcher_data;
共 3 条评论 - Geek_76f04f2020-06-02老师您好,我有个问题想咨询一下,我看资料说线程或者进程需要绑定内核,减少上下文切换,像这种reactor模型中,如果开辟corenum个线程,一般需要绑定内核吗?
作者回复: 我认为不需要
- 中年男子2020-04-24event_loop_init中,代码片段: event_loop_add_channel_event(eventLoop, eventLoop->socketPair[0], channel); 其中传入的应该是socketPair[1] 文稿中的代码还未修正,另外我认为这个fd作为参数实际上没有意义,event_loop_add_channel_event 往后调用的几个函数里实际上都用不到这个fd,只需要channel 就可以了,因为channel里已有这个fd。展开
作者回复: 代码已经修复,文稿我再看下哈。 你说的是对的,channel里确实有这个fd,这里是为了突出这个作用。
共 2 条评论 - wzp2020-02-19干货满满,有收获
作者回复: 👍