协程究竟比线程能省多少开销?

前文中中我们用实验的方式验证了Linux进程和线程的上下文切换开销,大约是3-5us之间。这个开销确实不算大,但是海量互联网服务端和一般的计算机程序相比,特点是:

  • 高并发:每秒钟需要处理成千上万的用户请求
  • 周期短:每个用户处理耗时越短越好,经常是ms级别的
  • 高网络IO:经常需要从其它机器上进行网络IO、如Redis、Mysql等等
  • 低计算:一般CPU密集型的计算操作并不多

即使3-5us的开销,如果上下文切换量特别大的话,也仍然会显得是有那么一些性能低下。例如之前的Web Server之Apache,就是这种模型下的软件产品。(其实当时Linux操作系统在设计的时候,目标是一个通用的操作系统,并不是专门针对服务端高并发来设计的)

为了避免频繁的上下文切换,还有一种异步非阻塞的开发模型。那就是用一个进程或线程去接收一大堆用户的请求,然后通过IO多路复用的方式来提高性能(进程或线程不阻塞,省去了上下文切换的开销)。Nginx和Node Js就是这种模型的典型代表产品。平心而论,从程序运行效率上来,这种模型最为机器友好,运行效率是最高的(比下面提到的协程开发模型要好)。所以Nginx已经取代了Apache成为了Web Server里的首选。但是这种编程模型的问题在于开发不友好,说白了就是过于机器化,离进程概念被抽象出来的初衷背道而驰。人类正常的线性思维被打乱,应用层开发们被逼得以非人类的思维去编写代码,代码调试也变得异常困难。

于是就有一些聪明的脑袋们继续在应用层又动起了主意,设计出了不需要进程/线程上下文切换的“线程”,协程。用协程去处理高并发的应用场景,既能够符合进程涉及的初衷,让开发者们用人类正常的线性的思维去处理自己的业务,也同样能够省去昂贵的进程/线程上下文切换的开销。因此可以说,协程就是Linux处理海量请求应用场景里的进程模型的一个很好的的补丁。

背景介绍完了,那么我想说的是,毕竟协程的封装虽然轻量,但是毕竟还是需要引入了一些额外的代价的。那么我们来看看这些额外的代价具体多小吧。

  • 1、协程切换CPU开销
    测试代码参见test05
# cd tests/test05/src/main/;  
# go build  
# ./main  
2019-08-08 22:35:13.415197171 +0800 CST m=+0.000286059
2019-08-08 22:35:13.655035993 +0800 CST m=+0.240124923

平均每次协程切换的开销是(655035993-415197171)/2000000=120ns。相对于前面文章测得的进程切换开销大约3.5us,大约是其的三十分之一。比系统调用的造成的开销还要低。

  • 2、协程内存开销
    在空间上,协程初始化创建的时候为其分配的栈有2KB。而线程栈要比这个数字大的多,可以通过ulimit 命令查看,一般都在几兆,作者的机器上是10M。如果对每个用户创建一个协程去处理,100万并发用户请求只需要2G内存就够了,而如果用线程模型则需要10T。
# ulimit -a  
stack size              (kbytes, -s) 10240  

本节结论

协程由于是在用户态来完成上下文切换的,所以切换耗时只有区区100ns多一些,比进程切换要高30倍。单个协程需要的栈内存也足够小,只需要2KB。所以,近几年来协程大火,在互联网后端的高并发场景里大放光彩。

无论是空间还是时间性能都比进程(线程)好这么多,那么Linus为啥不把它在操作系统里实现了多好?操作系统为了实现实时性更好的目的,对一些优先级比较高的进程是会抢占其它进程的CPU的。而协程无法实现这一点,还得依赖于挡前使用CPU的协程主动释放,于操作系统的实现目的不相吻合。所以协程的高效是以牺牲可抢占性为代价的。

扩展:由于go的协程调用起来太方便了,所以一些go的程序员就很随意地go来go去。要知道go这条指令在切换到协程之前,得先把协程创建出来。而一次创建加上调度开销就涨到400ns,差不多相当于一次系统调用的耗时了。虽然协程很高效,但是也不要乱用,否则go祖师爷Rob Pike花大精力优化出来的性能,被你随意一go又给葬送掉了。

参考文献

### 协程的调度机制与线程的差异 Kotlin 协程之所以被认为比 Java 线程更轻量,主要体现在其调度机制和资源消耗方式上。协程的创建、销毁和切换开销远小于 Java 线程,因为它们不需要依赖操作系统级别的线程调度。相反,协程的上下文切换是由 Kotlin 运行时管理的,而不是由操作系统调度器来处理。这种方式允许开发者在单个线程上运行成千上万的协程,而不会像线程那样消耗大量系统资源 [^2]。 ### 协程的执行模型 协程本质上是一种用户态的并发模型,它通过挂起和恢复的机制实现异步操作的顺序化处理。这种机制使得协程可以在不阻塞线程的情况下执行异步操作,从而提高并发效率。例如,一个协程在等待 I/O 操作完成时会自动挂起,释放当前线程给其他任务使用,待 I/O 操作完成后,协程会自动恢复执行。这种非阻塞式的执行模型有效减少了线程的数量和上下文切换的成本 [^1]。 ### 协程线程的资源消耗对比 尽管协程最终还是依赖线程来执行任务,但协程的轻量性体现在其对线程资源的高效利用上。Java 线程的创建和销毁涉及到内核态的操作,这通常伴随着较高的开销。相比之下,协程的生命周期管理完全在用户空间完成,避免了昂贵的系统调用。此外,由于协程可以在同一个线程上并发执行多个任务,因此减少了线程间的上下文切换次数,进一步降低了资源消耗 [^4]。 ### 协程的实际应用优势 在实际开发中,利用 Kotlin 协程可以显著简化异步编程模型,使得代码更加简洁易读。例如,可以使用 `launch` 和 `async` 构建器来启动协程,这些协程能够在不阻塞主线程的情况下执行耗时操作,并通过 `await()` 方法等待结果。这种方式不仅提高了程序的响应速度,还增强了用户体验 [^3]。 ```kotlin import kotlinx.coroutines.* fun main() = runBlocking { val job = launch { delay(1000L) println("World!") } println("Hello,") job.join() } ``` ### 协程的局限性 虽然协程提供了许多优势,但也存在一些局限性需要注意。例如,协程的性能优势主要体现在 I/O 密集型任务中,对于 CPU 密集型任务,过多的协程可能会导致线程过载。此外,协程的调试和异常处理机制相较于传统的线程模型更为复杂,需要开发者具备一定的经验和技巧 [^5]。 ###
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值