前言
近些年, 一些编程语言的新贵Go和Kotlin纷纷引入了协程这个语言特性, 使得协程这个似乎十分陌生的概念开始频繁进入大家的视野, 为了便于理解, 开发者们都把它当作线程的小弟来对待, 即轻量级线程. 可是真要细说起来, 协程其实是很早就出现的一个编程概念, 它的出现甚至是是早于线程的, 但是就编程语言的江湖地位而言, 协程是不如线程的, 所以向线程低头叫爸爸不奇怪. 在我们了解了进程和线程之后, 学习协程将会非常简单.
在使用线程时, 可以最大限度的压榨CPU, 实现并行运行程序, 大大提升效率. 而使用协程, 实际上并没有提升程序的执行效率, 一个线程下的所有协程在任意瞬间都仅仅只能执行一个协程(无论是否多核), 即协程并不能像进程线程一样缩短执行时间. 那么为什么要用到线程? 为什么线程能处理高并发呢?
实际上, 在操作系统的历史上, 协程的出现时间是比线程要早的, 那么协程又是如何提出的? 它又解决了什么问题?
用户级线程
很久很久之前,线程的概念出现了,但操作系统厂商可不能直接就去修改操作系统的内核,因为对他们来说,稳定性是最重要的。贸然把未经验证的东西加入内核,出问题了怎么办?所以想要验证线程的可用性,得另想办法。
在最早期线程还未被操作系统厂商完成那会, 我们使用pthraed线程库, 实际上用的是用户级的线程. 是位于用户空间的,操作系统内核对这个库一无所知,所以从内核的角度看,它还是按正常的方式管理.
也就是说操作系统眼里还是只有进程, 用线程库写的多线程进程,只能一次在一个 CPU 核心上运行. 这其实是用户级线程的一个缺点,这些线程只能占用一个核,所以做不到并行加速,而且由于用户线程的透明性,操作系统是不能主动切换线程的,换句话讲,如果线程 A 正在运行,线程 B 想要运行的话,只能等待 A 主动放弃 CPU. 即使有线程库,用户级线程也做不到像进程那样的轮转调度.
注:对操作系统来说,用户级线程具有不可见性,也称透明性。
虽然不能做到轮转调度,但用户级线程也有他自己的好处——你可以为你的应用程序定制调度算法,毕竟什么时候退出线程你自己说了算。
用户级线程示意图:
内核级线程
有了用户级线程的铺垫,内核级线程就好讲多了。现在我们知道,许多操作系统都已经支持内核级线程了。为了实现线程,内核里就需要有用来记录系统里所有线程的线程表。当需要创建一个新线程的时候,就需要进行一个系统调用,然后由操作系统进行线程表的更新。当然了,传统的进程表也还是有的。如果操作系统「看得见」线程,有什么好处?
操作系统内核如果知道线程的存在,就可以像调度多个进程一样,把这些线程放在好几个 CPU 核心上,就能做到实际上的并行了. 如果线程可见,那么假如线程 A 阻塞了,与他同属一个进程的线程也不会被阻塞。这是内核级线程的绝对优势.
内核级线程示意图:
综上, 我们可以看到, 厂商在最初设计时先是设计了运行在用户级的线程(协程), 然后才设计初我们真正意义上的线程. 那么协程使用的意义又在哪里?
线程的缺点
让操作系统进行线程调度,那意味着每次切换线程,就需要「陷入」内核态,而操作系统从用户态到内核态的转变是有开销的(上下文的切换),所以说内核级线程切换的代价要比用户级线程大.而运行在用户空间的用户级线程就没有这个问题. 还有很重要的一点——线程表是存放在操作系统固定的表格空间或者堆栈空间里,所以内核级线程的数量是有限的,扩展性比不上用户级线程.
总结
大家对于协程的理解有很多分歧,但是对我而言,协程其实得分两个阶段来理解
在协程诞生之初,只是用来解决编程中的某些特殊问题的编程组件,它的多任务更像多个函数的组合协作执行,那个时候,协程其实更像是一种具备暂停恢复的函数.但是这种功能似乎并不受欢迎,因此协程在很长一段时间内都是比较小众的.(此时协程和线程关系并不大)
如今它成为底层支持多线程的协作式多任务组件,很好的解决了线程协作的痛点,同时也逐渐变得越来越受欢迎,协程和线程的关系更加亲密,它们似乎也变得更加相似.(如今你可以把协程视作一种轻量级线程)
而协程的发展历程,其实也就是经历了这两个阶段。