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

33 | 自己动手写高性能HTTP服务器(二):I/O模型和多线程模型实现

子线程被唤醒后执行event_loop_handle_pending_channel处理新增的channel事件列表
主线程调用event_loop_do_channel_event增加channel对象到子线程
使用socketpair或pipe管道唤醒子线程
子线程调用event_loop_thread_run完成初始化并通知主线程
主线程调用event_loop_thread_start初始化子线程
使用mutex和condition解决多线程通知问题
改进线程选择算法
修改代码让sub-reactor默认的线程个数为cpu*2
增加已连接套接字事件到sub-reactor线程中
主线程等待多个sub-reactor子线程初始化完
思考题
多线程设计的几个考虑
总结

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

你好,我是盛延敏,这里是网络编程实战第 33 讲,欢迎回来。
这一讲,我们延续第 32 讲的话题,继续解析高性能网络编程框架的 I/O 模型和多线程模型设计部分。

多线程设计的几个考虑

在我们的设计中,main reactor 线程是一个 acceptor 线程,这个线程一旦创建,会以 event_loop 形式阻塞在 event_dispatcher 的 dispatch 方法上,实际上,它在等待监听套接字上的事件发生,也就是已完成的连接,一旦有连接完成,就会创建出连接对象 tcp_connection,以及 channel 对象等。
当用户期望使用多个 sub-reactor 子线程时,主线程会创建多个子线程,每个子线程在创建之后,按照主线程指定的启动函数立即运行,并进行初始化。随之而来的问题是,主线程如何判断子线程已经完成初始化并启动,继续执行下去呢?这是一个需要解决的重点问题。
在设置了多个线程的情况下,需要将新创建的已连接套接字对应的读写事件交给一个 sub-reactor 线程处理。所以,这里从 thread_pool 中取出一个线程,通知这个线程有新的事件加入。而这个线程很可能是处于事件分发的阻塞调用之中,如何协调主线程数据写入给子线程,这是另一个需要解决的重点问题。
确认放弃笔记?
放弃后所记笔记将不保留。
新功能上线,你的历史笔记已初始化为私密笔记,是否一键批量公开?
批量公开的笔记不会为你同步至部落
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
  • 深入了解
  • 翻译
    • 英语
    • 中文简体
    • 中文繁体
    • 法语
    • 德语
    • 日语
    • 韩语
    • 俄语
    • 西班牙语
    • 阿拉伯语
  • 解释
  • 总结

本文介绍了如何设计高性能网络编程框架的I/O模型和多线程模型实现。在多线程设计中,主要考虑了主线程等待多个sub-reactor子线程初始化完的问题。为了解决这个问题,文章使用了mutex和condition两个主要武器,通过加锁和信号量的方式实现了主线程和子线程的同步。主线程在等待每个子线程完成初始化时,通过加锁和检查event_loop_thread的eventLoop对象是否为非NULL值来判断子线程是否已经初始化完成,从而决定是否继续往下执行。同时,文章还介绍了子线程的执行函数event_loop_thread_run,以及子线程和主线程共享的eventLoop对象。通过这些方法,成功解决了多线程设计中的同步问题。 在文章中还介绍了如何将已连接套接字事件交给sub-reactor子线程处理,以提高处理效率。通过构建类似管道一样的描述字,让event_dispatcher注册该管道描述字,当需要唤醒sub-reactor线程时,往管道上发送一个字符即可。同时,文章还提到了如何处理连接已建立的回调函数,以及如何在主线程中创建tcp_connection对象并将其注册到子线程的event_loop中。 总的来说,本文重点讲解了框架中涉及多线程的两个重要问题,以及通过锁、信号量和管道等方式解决了这些问题。读者可以通过本文了解多线程设计的关键问题和解决方法,以及如何提高网络编程框架的性能。

仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《网络编程实战》
新⼈⾸单¥59
立即购买
登录 后留言

全部留言(26)

  • 最新
  • 精选
  • 酸葡萄
    老师,你好,有个地方不是很明白, 为什么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就对应一个线程。

    2019-12-11
    6
  • 时间
    线程池个数有限,如何处理成千上万的链接?假如线程池共四个线程,正在处理四个链接。再来一个链接如何处理呢?

    作者回复: 好问题。 线程不是每时每刻都要干活的,就好比一个流水线工人,只有轮到他的时候,他才需要出力干活。如果这四个连接都在干活,那第五个只好等任意一个线程空闲出来。 所有的事情,秘诀都在于"分时复用",比如你的cpu,也就4个core,为啥同时可以打游戏,写文稿,看电影,跑程序.....想通了这个,就想通了如何处理多个连接了。

    2020-09-23
    4
    5
  • YUAN
    为什么不直接让子线程自己调用accept而要主线程调用呢?

    作者回复: 那主线程干啥呢?

    2020-12-11
    4
  • 郑祖煌
    第一道, 可以直接在应用层上将输入的线程个数*2 。 第二道,(1)可以判断已经创建好的线程 那个线程的事件个数最少,挂在事件最少的那个线程上。

    作者回复: 👍

    2020-07-15
    2
  • 消失的时光
    老师你好,不是很理解为什么要socketpair唤醒,直接把新连接的socket加到epoll里面,有发送就的数据过来,这个线程自己不会醒吗?

    作者回复: epoll不允许这么干。

    2022-01-07
    4
    1
  • keepgoing
    想问问老师关于基础语法的问题,代码里很多地方对象都是相互引用的,比如tcp_connection里引用了channel指针, channel 对象里引用了tcp_connection指针, dispatcher里引用了event_loop指针, event_loop里也引用了dispatcher指针。这样代码编译的时候为什么不会引起报错。。

    作者回复: 为什么会报错呢?每个指针就是一个内存地址。

    2020-08-24
    2
    1
  • MoonGod
    老师关于加锁这里有个疑问,如果加锁的目的是让主线程等待子线程初始化event loop。那不加锁不是也可以达到这个目的吗?主线程while 循环里面不断判断子线程的event loop是否不为null不就可以了?为啥一定要加一把锁呢?

    作者回复: 好问题, 我答疑统一回答吧。

    2019-10-23
    2
    1
  • 鱼向北游
    netty选子线程是两种算法,都是有个原子自增计数,如果线程数不是2的幂用取模,如果是就是按位与线程数减一

    作者回复: 嗯,涨知识了,代码贴一个?

    2019-10-23
    3
    1
  • 雨里
    没有看明白主从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; }

    2022-02-28
    2
  • zssdhr
    老师,关于 event_loop_thread 有两个问题。 1. 为什么主线程要等待子线程初始化完成?是担心 tcp_server_init 后、但子线程还未初始化完成时,thread_pool_get_loop 无法找到子线程来处理新来的连接吗? 2. 文中提到”你可能会问,主线程是循环在等待每个子线程完成初始化,如果进入第二个循环,等待第二个子线程完成初始化,而此时第二个子线程已经初始化完成了,该怎么办?“ 主线程不是等第一个子线程初始化完成后才会进入下一个循环启动第二个子线程吗?怎么会出现”而此时第二个子线程已经初始化完成了“?

    作者回复: 1.可以这么认为; 2.你说的没错,是按照顺序的,第一个完成,再第二个,问题是到第二个以后,如何判断第二个已经完成初始化了,因为这里的线程都是异步的,所以有可能线程初始化完成了,才进入判断。

    2022-02-20
收起评论
显示
设置
留言
26
收藏
沉浸
阅读
分享
手机端
快捷键
回顶部