当前位置:首页 > 技术学院 > 技术前线
[导读]协程是近几年后端开发领域最火热的技术概念之一,从Go语言的原生协程,到Python的async/await生态,几乎所有主流编程语言都在原生层面加入了协程支持。很多开发者天天在代码里写协程,却始终没有彻底想明白一个核心问题:协程到底解决了什么之前所有IO模式都解决不好的痛点?要彻底搞懂协程的价值,你必须顺着IO模型的演化路径,从最原始的阻塞IO一路看到现代异步IO,看清每一代IO模式的优势和缺陷,最后你会发现协程是站在所有前人肩膀上,兼顾开发效率和运行性能的最优解。

协程是近几年后端开发领域最火热的技术概念之一,从Go语言的原生协程,到Python的async/await生态,几乎所有主流编程语言都在原生层面加入了协程支持。很多开发者天天在代码里写协程,却始终没有彻底想明白一个核心问题:协程到底解决了什么之前所有IO模式都解决不好的痛点?要彻底搞懂协程的价值,你必须顺着IO模型的演化路径,从最原始的阻塞IO一路看到现代异步IO,看清每一代IO模式的优势和缺陷,最后你会发现协程是站在所有前人肩膀上,兼顾开发效率和运行性能的最优解。

一、IO模式的演化路径:从单线程阻塞到多线程并发

最早的原始IO模式就是单线程同步阻塞模式,整个程序只有一个执行流,一次只能处理一个IO任务,发起IO操作之后整个线程就完全阻塞住,一直等到IO操作完成才能继续往下执行。这种模式的代码逻辑最简单,完全没有任何并发相关的复杂度,但是性能差到极致,同一时间只能处理一个连接,稍微多来几个用户请求,整个服务就直接卡住,完全没有任何并发承载能力,只能用来跑最基础的演示程序,根本没办法用到实际的生产环境里。

为了解决单线程模式的并发问题,第二代多进程同步阻塞模式诞生了。每来一个新的用户连接,操作系统就直接fork一个新的进程专门负责处理这个连接,不同进程之间完全独立互不干扰,哪怕一个进程阻塞住,也不会影响其他连接的处理。这种模式的并发能力比单线程模式强了很多,但是进程的创建和销毁成本极高,进程之间的上下文切换开销非常大,同时系统能同时承载的进程数量极其有限,并发连接数稍微上去一点,系统的CPU资源就会被进程切换吃光,完全没办法支撑大规模的高并发场景。

第三代多线程同步阻塞模式,用更轻量的线程替代了进程。线程是操作系统调度的最小单位,同一个进程里的多个线程共享进程的地址空间,线程的创建销毁成本和上下文切换开销,都比进程小很多。每来一个新连接就创建一个新线程处理,系统能承载的并发数量比多进程模式提升了一个数量级。但是当并发连接数达到几万级别之后,大量线程长时间处于阻塞休眠状态,绝大多数CPU时间都浪费在线程的上下文切换上,内存也被海量线程的栈空间大量占用,系统的整体性能会出现断崖式下跌,根本无法承载十万级别的大规模并发。

二、IO多路复用与异步回调的优势与天生缺陷

第四代IO多路复用模式,彻底抛弃了一个连接配一个线程的思路,用少量线程同时监控海量的IO连接状态。一个线程通过epoll这类多路复用机制,同时看管几万甚至几十万个连接,哪个连接的IO事件就绪了,线程就去处理哪个连接,没有事件就绪的时候线程直接休眠,完全不会做无用功。这种模式的性能天花板被直接拉高了一个数量级,少量几个线程就可以支撑几十万级别的并发连接,是现在绝大多数高性能网络服务的底层基础。

但是IO多路复用模式有一个非常致命的缺陷:它要求所有的IO操作都必须是非阻塞的,整个代码的执行逻辑完全基于事件回调驱动。原本线性顺次的业务逻辑,被拆成了一个个零散的事件回调函数,原本一行就能写完的“发起请求-等待响应-处理结果”的逻辑,被拆成了好几个分散的回调,代码的执行流完全跳来跳去,逻辑链路被切割得支离破碎,也就是大家常说的“回调地狱”。代码的可读性、可维护性大幅下降,异常处理逻辑极其难写,普通开发者根本驾驭不了这种模式,很容易写出大量难以调试的bug。

第五代异步IO模式,在语言层面加入了async/await语法糖,把回调地狱的问题做了表层的封装。开发者可以用近似同步代码的线性写法来写异步逻辑,不需要手动写零散的回调函数,代码的可读性得到了很大的提升。但是这种模式本质上还是基于事件循环的异步回调,await关键字本质上是把当前的执行上下文打包挂起,注册成后续的回调,底层的执行逻辑依然是事件驱动的。开发者必须全程小心翼翼地处理所有异步接口,一旦不小心在异步函数里写了一个阻塞操作,就会直接卡住整个事件循环,导致所有连接都被拖慢,整个编程模型对开发者的约束非常强,稍有不慎就会踩坑。

三、协程:兼顾性能与开发效率的终极最优解

协程本质上是用户态轻量级执行流,它完全绕过了操作系统内核的线程调度,所有的调度逻辑全部在用户态的语言 runtime 里完成。一个协程的栈内存开销只有几KB,一个普通的服务器进程里,轻轻松松就能跑几百万个协程,完全没有多线程模式下的内存开销压力。协程的上下文切换全程都在用户态完成,不需要陷入内核,没有任何系统调用的开销,切换的成本比操作系统线程的切换低了两个数量级,哪怕同时跑几百万个协程,CPU的开销也几乎可以忽略不计。

协程最核心的价值,是彻底把高并发编程的复杂度对普通开发者屏蔽掉了。你写协程代码的时候,完全可以用最自然的同步线性写法,不需要写任何回调,也不需要手动处理事件循环,代码的逻辑和普通的同步代码一模一样,可读性极高。但是当你在协程里调用任何一个协程化的IO操作的时候,runtime会自动在后台帮你把当前协程挂起,让出CPU去执行其他就绪的协程,等到IO操作完成之后,再自动把之前挂起的协程唤醒,从之前暂停的位置继续往下执行。整个IO等待的过程完全不会阻塞任何底层的执行线程,所有的调度、挂起、唤醒操作全部由runtime自动处理,开发者完全感知不到背后的异步逻辑,不需要任何额外的心智负担。

协程既保留了IO多路复用模式极致的高并发性能,单进程就能轻松支撑几十万甚至上百万的并发连接,又保留了同步阻塞模式简单直观的代码写法,彻底解决了之前几代IO模式里“性能和开发效率不可兼得”的核心矛盾。你完全不需要像写异步回调代码那样,时刻小心翼翼地规避所有阻塞操作,也不需要像多线程模式那样,为了处理大量线程的同步互斥写大量复杂的锁逻辑,协程天然的协作式调度,从根源上避免了大部分多线程里的并发安全问题。

现在Go语言的原生协程生态之所以能在后端领域快速普及,核心原因就是它把高并发编程的门槛降到了普通开发者可以轻松驾驭的程度。开发者不需要深入理解epoll的底层原理,不需要精通异步回调的事件驱动逻辑,只需要按照普通同步代码的思路写业务,就能轻松写出支撑十万级并发的高性能服务,这是之前所有IO模式都不可能做到的事情。

协程不是什么凭空诞生的黑魔法,它是几代IO模式演化下来,最终平衡了性能、开发效率、资源开销的最优解,也是现在高并发服务开发的主流技术方向。

本站声明: 本文章由作者或相关机构授权发布,目的在于传递更多信息,并不代表本站赞同其观点,本站亦不保证或承诺内容真实性等。需要转载请联系该专栏作者,如若文章内容侵犯您的权益,请及时联系本站删除( 邮箱:macysun@21ic.com )。
关闭