谈谈多路转接实现的服务器

单线程服务器

对单线程阻塞的服务器,每个连接和相关处理动作会阻塞下一个请求的接收

只有上一个请求处理完成,才能处理下一个请求

通过多路转接,使得服务器能够并发处理多个请求,这些请求与事件处理函数绑定并注册到事件循环上

每次循环,从多路转接函数获得触发事件的请求(即套接字),根据触发事件类型顺序调用相应的处理函数

以上就要求这些处理函数本身不能阻塞,或者说通过某些手段实现非阻塞

常规的实现方式有:
1. 通过线程池提交任务,执行CPU阻塞调用并提供回调
2. 通过中间件实现任务异步化
3. 通过再次注册事件

所谓的高速服务器一般指处理请求本身,不包括服务器自身状态维护
如Nginx、Redis,若要对服务器状态进行维护,一般会通过子进程或子线程等方式进行

为什么多路转接速度快?

首先从阻塞说起,网络IO造成的阻塞时间相比CPU处理速度来说,过于漫长,大部分时间都在等待网络IO

通过多路转接,一旦发生IO阻塞,可以立即切换到其他事件处理函数中,使得处理效率提升

其次是开销,传统通过进程、线程实现请求处理,让系统自动调度,小型应用完全可以接受

一旦请求量提升或者说遇到高并发,进程/线程执行栈切换开销就很明显,另外进程/线程本身内存开销也很大

早期解决方案是通过池化技术免去创建和销毁部分的开销,但依然无法解决高并发

毕竟一个系统能创建的进程或线程数量不会太多,池化后可使用的量也不会很多,并发量上来问题还是明显

一个解决的思路是分布式,利用LB做多机,但又引入了额外的分布式问题

另一个解决思路是降低开销,将内核上的切换引入到用户态,降低切换开销,通过协程实现单线程并发

单线程(协程)其实本质是一种由用户控制的切换,遇到IO就切换,让系统去等待数据并通知

只要是遇到网络IO,基本离不开多路转接,因为没有比它更高效的轮询机制了

用上了多路转接和协程,就能实现内存占用低的同时保证效率的提升(毕竟单线程实现),当然不使用协程也能得到足够的效率提升

毕竟实现协程还要连带实现一系列的相关机制,往往线程池就足以

另外谈谈Golang的goroutine

虽然也叫协程,但Go语言本身的设计架构是M:N的

有一个runtime负责逻辑线程到物理线程的映射,在Go中,提交一个G(goroutine),会被添加到P(processor)的一个队列中,P从队列中循环取出goroutine执行,而P又与M进行映射,P就是逻辑线程,M则是物理线程

等于说,Go从语言层面实现了多核心并发,并且因为执行栈都是在用户态管理(半用户态,实际是runtime维护了一个内存池),遇到网络IO阻塞,goroutine会被runtime从P队列中取出挂起,通过专门负责网络IO的G去多路转接监听事件,当事件到达则将这个goroutine按某个策略重新添加到某个P的队列(runtime的调度策略)

从执行流的角度理解,每个goroutine相当于一个执行流,一个P对应一组执行流,物理线程M上运行P,运行过程中遇到网络IO阻塞或其他调度权转让(从goroutine到goroutine),由runtime负责执行流的跳转,对物理线程M是无感知的

原文地址:https://www.cnblogs.com/ikct2017/p/13143446.html