多路IO复用事件驱动的服务器模型比阻塞IO多线程服务器模型高在哪?

loop_0 · 2020-05-28 17:28:25 · 940 次点击 · 预计阅读时间 3 分钟 · 大约8小时之前 开始浏览    
这是一个创建于 2020-05-28 17:28:25 的文章,其中的信息可能已经有所发展或是发生改变。

  • 本文从两点进行分析:
    • 线程调度
    • 内存占用
  • 文章很多内容都是仅凭个人认知推测, 并未进行实时考证, 不过个人感觉应该有参考价值(不然也不会写了)

线程调度

linux进程调度的时机

  • 进程不再处于TASK_RUNNING状态

    • 如: 由于等待IO而阻塞(TASK_UNINTERRUPTIBLE),或者因等待资源和特定事件而休眠(TASK_INTERRUPTIBLE),又或者被debug/trace设置为TASK_STOPPED/TASK_TRACED状态。
  • 周期性的时钟中断处理函数中, 判断进程被抢占(preemption)(取决于preemption算法)

    xxx_irq_xxx(){
        ...{
            twd_handler(){
                ...{
                    update_process_times(){
                        scheduler_tick(){
                            ...{
                                update_curr() //更新时间片计数
    
                                //检查是否被抢占(preemption)(取决于`preemption`算法)
                                //如:进程时间片达到最大值,有优先级更高的进程
                                //判断是否需要重新调度,需要则设置TIF_NEED_RESCHED标志
                                check_preempt_tick()
                            }
                        }
                    }
                }     
            }
        }
    
        ret_to_user_from_irq (){
            slow_work_pending (){
                if TIF_NEED_RESCHED {
                    do_work_pending(){
                        schedule() //进程调度
                    }
                }
            }
        }
    }
    
  • 新进程创建的时候, 判断进程被抢占(preemption)(取决于preemption算法)

  • ......

linux的线程

  • 其实linux最初是不支持线程的, 好像unix标准中就没有线程, 线程是在Posix标准规定的(貌似,记不太得), linux中的线程其实就是进程(轻量级进程), 线程的概念最初来源于windows, 这里不作深究。

为什么多线程阻塞io服务器模型会频繁触发进程调度

  • 每次io操作, 进程变为睡眠状态, 触发进程调度。
  • 周期性的时钟中断处理函数中, 会判断进程是否被抢占(preemption), 这取决于preemption算法(个人推测 4 核 40个进程 肯定 比 4 核 16个进程触发的次数多)。

  • 虽然使用了线程池, 但是还是免不了创建新线程, 可能会触发进程调度。

  • 另外需要注意的一点是, 多线程阻塞io服务器模型不仅会频繁触发进程调度, 而且每次调度的进程数量(虽然有一部分进程处于TASK_UNINTERRUPTIBLE,TASK_INTERRUPTIBLE状态不需要被调度)要比事件驱动服务器模型的进程数量要多很多, 那么调度的时间是否也会变长, 当然这取决于进程调度算法,这里不作深究。

内存占用

  • 一个线程至少要占用固定大小的内存作为函数栈, 还有一些内核结构体, 相对来说事件驱动模型更节省内存占用

Golang- 用户态线程(goroutine)调度

  • 问题: goroutine是如何进行调度的呢?

    • 首先linux进程调度执行时机:

      • 从系统调用(syscall)返回用户态时
      • 从中断返回用户态时
    • 显然golang程序不可能基于时钟中断, 系统调用, 那么go语言在什么时候进行进程调度呢?

      • 个人认为, go语言的所有io操作在底层实现都采用多路IO多路转接,而并非 阻塞IO(因为这会触发系统进程调度), 当goroutine进行io操作, channel读写时(可能还有其他操作)时, 触发goroutine调度。

      • 所以goroutinue没有强制的调度方式, 此时拿 多路IO复用事件驱动的服务器模型 做一下对比, 神似, 事件分发 换成 用户态线程调度, goroutinue没有强制的调度方式, 在一些文章中也有体现:

        P与M一般也是一一对应的。他们关系是: P管理着一组G挂载在M上运行。当一个G长久阻塞在一个M上时,runtime会新建一个M,阻塞G所在的P会把其他的G 挂载在新建的M上。当旧的G阻塞完成或者认为其已经死掉时 回收旧的M。

    • 个人结论:

      • go语言的服务器模型是基于多路io复用以及用户态线程(goroutine)调度的高并发服务器模型。
      • 用户态线程调度比系统进程调度好在哪里呢, 我觉得是非强制的调度方式, 操作系统时钟中断的抢占调度算法是为了尽量公平的分配cpu资源, 而非强制的调度方式, 不在乎公平, 更多在乎性能, 使cpu占用更多的跑在功能代码上, 提高并发。
      • 我认为, 这种用户态的线程调度在性能上还是比不过事件驱动, 是两种服务器模型的折中, 提高了性能,降低了开发难度, 非常复杂的设计, 不愧为 为编写高性能高并发服务器设计的语言。

内容参考


有疑问加站长微信联系(非本文作者))

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

940 次点击  ∙  1 赞  
加入收藏 微博
2 回复  |  直到 2020-06-04 09:59:45
loop_0
loop_0 · #1 · 5年之前

看到很多文章证明了自己的猜想, 分享一下: golang net包里的异步IO实现原理分析: http://blog.sina.com.cn/s/blog_630c58cb010188id.html golang 如何将多路复异步io转变成阻塞io的: https://blog.csdn.net/d_guco/article/details/75150696

huangxianghan
huangxianghan · #2 · 5年之前

真正实现事件驱动的也只有Windows的iocp而已,liunx的epoll并不是。

添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传