协程的概念总结

Posted OshynSong

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了协程的概念总结相关的知识,希望对你有一定的参考价值。

并发

最早的计算机,每次只能执行一个程序,只有当当前执行的程序结束后才能执行其它程序,在此期间,别的程序都得等着。到后来,计算机运行速度提高了,程序员们发现,单任务运行一旦陷入IO阻塞状态,CPU就没事做了,很是浪费资源,于是就想要同一时间执行那么三五个程序,几个程序一块跑,于是就有了并发。原理就是将CPU时间分片,分别用来运行多个程序,可以看成是多个独立的逻辑流,由于能快速切换逻辑流,看起来就像是大家一块跑的。

并发解决了两个问题:

  1. 提高了CPU的利用率,在某个程序陷入IO或者其它等待状态时,CPU可以转而执行其它程序。
  2. 表面上看起来多个程序一起运行,解决了跑程序排队等待的问题。

引入的新问题:

并发执行也存在一些问题。我的程序运行到一半,别的进程突然插进来,抢占了CPU,我的中间状态怎么办,我用来存储的内存被覆盖了怎么办?所以跑在一个CPU里面的并发都需要处理上下文切换的问题。进程就是这样抽象出来了一个概念,搭配虚拟内存、进程表之类的东西,用来管理独立运行的程序运行、切换。因为程序的使用涉及到大量的计算机资源配置, 把这活随意的交给用户程序,容易让整个系统被搞挂,资源分配也很难做到相对的公平。所以就出现了操作系统,核心的操作需要陷入内核(kernel),切换到操作系统,让内核来做。

上下文切换

上下文切换最早是指进程的上下文切换(context switch),它发生在内核态。内核调度器会对每个CPU上执行的进程进行调度(scheduling),以保证每个进程都能分到CPU时间片。当一个进程的时间片用完,或被中断后,内核将保存该进程的运行状态(即上下文),将其存入运行队列(run queue),同时让新的进程占用CPU。进程的上下文切换包括内存地址空间、内核态堆栈和硬件上下文(CPU寄存器)的切换,所以代价很高。

线程

有的时候碰着I/O访问,阻塞了后面所有的计算。空着也是空着,内核就直接把CPU切换到其他进程,让人家先用着。当然除了I\\O阻塞,还有时钟阻塞等等。一开始大家都这样弄,后来发现太慢了,一切换进程得反复进入内核,置换掉一大堆状态。进程数一高,大部分系统资源就被进程切换给吃掉了。由于进程切换开销大,所以设计了线程。 大致意思就是,这个地方阻塞了,但我还有其他地方的逻辑流可以计算,这些逻辑流是共享一个地址空间的,不用特别麻烦的重新加载地址空间,页表缓冲区,只要把寄存器刷新一遍就行,能比切换进程开销少点。Linux 2.6内核的clone()系统调用已经支持创建内核级线程,且发布了内核线程库pthread。在同一进程内的线程可以共享进程的地址空间,线程仅需要维护自己的寄存器、栈和线程相关的变量。不过内核线程的调度仍然需要由内核完成,这需要进行用户态和内核态的模式切换,至少包括堆栈和内存映射的切换。而且,不同进程之间的线程切换,有可能会还会导致进程切换,所以代价还是不小。

协程

为了进一步减小内核态线程上下文切换的开销,于是又有了用户态线程设计,即纤程(Fiber)。如果连时钟阻塞、 线程切换这些功能我们都不需要了,自己在进程里面写一个逻辑流调度的东西。那么我们即可以利用到并发优势,又可以避免反复系统调用,还有进程切换造成的开销,分分钟给你上几千个逻辑流不费力。这就是用户态线程。

从上面可以看到,实现一个用户态线程有两个必须要处理的问题:

  1. 碰着阻塞式I\\O会导致整个进程被挂起;
  2. 由于缺乏时钟阻塞,进程需要自己拥有调度线程的能力。

如果一种实现使得每个线程需要自己通过调用某个方法,主动交出控制权。那么我们就称这种用户态线程是协作式的,即是协程。协程的做法很像早期操作系统的协作式多任务。

协作式多任务:当任务得一个到了 CPU 时间,除非它自己放弃使用 CPU ,否则将完全霸占 CPU。win3.x就是这个方式。
但是,对于操作系统来说,这种做法会让系统不稳定。因为操作系统管理者整个计算机的资源,这个做法容易让系统失去控制(比如用户程序的一个死循环),因此,现在的操作系统都是用的是抢占式多任务。而在一个程序内,使用协作式的方法是可行的,因为自己的程序可以自己控制。 可以这么理解:协程就是在用户程序中实现了协作式任务调度。 这里输入引用文本进程、线程、协程的设计,都是为了并发任务能够更好的利用CPU资源,协程可以作为进程和线程的有力补充。由于我们可以在用户态调度协程任务,所以,我们可以把一组互相依赖的任务设计成协程。这样,当一个协程任务完成之后,可以手动进行任务调度,把自己挂起(yield),切换到另外一个协程执行。这样,由于我们可以控制程序主动让出资源,很多情况下将不需要对资源加锁。

协程(coroutine)顾名思义就是“协作的例程”(co-operative routines)。跟具有操作系统概念的线程不一样,协程是在用户空间利用程序语言的语法语义就能实现逻辑上类似多任务的编程技巧。实际上协程的概念比线程还要早,按照 Knuth 的说法“子例程是协程的特例”,一个子例程就是一次子函数调用,那么实际上协程就是类函数一样的程序组件,你可以在一个线程里面轻松创建数十万个协程,就像数十万次函数调用一样。只不过子例程只有一个调用入口起始点,返回之后就结束了,而协程入口既可以是起始点,又可以从上一个返回点继续执行,也就是说协程之间可以通过 yield 方式转移执行权,对称(symmetric)、平级地调用对方,而不是像例程那样上下级调用关系。当然 Knuth 的“特例”指的是协程也可以模拟例程那样实现上下级调用关系,这就叫非对称协程(asymmetric coroutines)。

协程的优势:

  • 最大的优势就是协程极高的执行效率。因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显。
  • 第二大优势就是不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。

因为协程是一个线程执行,那怎么利用多核CPU呢?最简单的方法是多进程+协程,既充分利用多核,又充分发挥协程的高效率,可获得极高的性能。

以上是关于协程的概念总结的主要内容,如果未能解决你的问题,请参考以下文章

协程的概念总结

深入理解Kotlin协程协程的上下文 CoroutineContext

二十协程

Python协程与JavaScript协程的对比

Kotlin 协程协程的挂起和恢复 ① ( 协程的挂起和恢复概念 | 协程的 suspend 挂起函数 )

Kotlin 协程协程的挂起和恢复 ① ( 协程的挂起和恢复概念 | 协程的 suspend 挂起函数 )