我们在之前的文章中讲过,在并发场景下,传统的基于多线程的命令式开发模型虽然比较简单,但并发数高了之后资源占用较高,大量线程会阻塞;而响应式编程模式我们可以通过异步化处理提升系统资源的利用效率,但异步开发有违人的直觉,门槛比较高。作为成年人,我们肯定希望全都要呀,那么能实现吗?今天我们就来介绍另一种并发的开发模式—协程
背景知识
在正式介绍协程的定义时,我们还需要先了解一些操作系统的基础知识:
用户态与内核态
在现代的操作系统中,为了有效减少内核资源的访问和冲突,一般会将功能划分为不同的层级,与硬件关系紧密相关的模块(中断处理)以及运行频率较高的模块(时钟,进度调度等),都会将它们常驻内存,这些就构成了通常所谓的OS内核,这样安排主要基于两个目的:
- 有效减少内核资源的访问及冲突,提升访问效率
- 对这些软件进行保护,防止遭受其他应用程序的破坏
因此OS的运行状态又分为了两种:
- 内核态:它具有较高的特权,能执行一切指令,访问所有的寄存器和存储区,OS内核都运行在内核态;
- 用户态:它具有较低特权的执行状态,仅能执行规定的指令,访问指定的寄存器和存储区。一般情况下,应用程序只能在用户态运行。
用户态的应用程序有时候也需要运行一些敏感操作,比如访问硬件(磁盘,网卡等),创建进程等,这些操作必须要通过一种叫做系统调用的形式去实现,系统调用可以看作是OS内核暴露给用户态应用程序的一些接口和函数,这里不做过多解释。
当用户态应用程序通过系统调用访问内核态的资源时,就涉及到运行上下文的切换,这种切换是有开销的,一般需要几十纳秒到数微秒左右的时间。
N:M线程模型
有了上面的介绍,我们可以再回顾一下我们经常使用的线程的概念,线程其实是为了并发和资源共享从而抽象出来的一种基本的调度单元,那谁来调度和管理线程呢?其实也可以分成两类:
- 用户线程:由编程语言或者应用程序自行管理和调度,不需要内核的支持,内核也不会感知到,消耗资源非常少。
- 内核线程:由操作系统创建,管理以及调度,只运行在内核态,资源消耗较大。
但单纯的用户线程没有什么意义,因为从操作系统的视角,只能感知到内核线程,所有的系统的调用和计算也只能通过内核线程才能完成,所以用户线程必须得映射到内核线程。这就是所谓得N:M线程模型,N是用户线程,M是内核线程,Java在1.2版本前是N:1的,即所有的用户线程都会对应到一个内核线程中,这样做线程的操作较快且消耗较低,并且线程数量不受操作系统限制。但缺点也很明显,由于只有一个内核线程,所谓的并发都是伪并发,只有内核线程阻塞了,其上的所有用户线程都会阻塞,更无法发挥多核CPU的优势
1.2之后的Java版本都是1:1的映射模式,即一个用户线程对应一个内核线程,这样每个线程的创建、调度、销毁都需要内核的支持,每次线程的创建、切换都会设计用户状态/内核状态的切换,性能开销比较大,都能够真正利用多核CPU的能力。
线程的调度
为了有效的使用系统资源,确保公平性和及时响应,线程是需要经过调度才能执行的,不然就乱套了。主流的调度算法一般有两种:
- 抢占式调度: 管理者能在任意时候中断正在运行的任务,并将CPU分配给另一个任务。
- 协作式调度: 任务在适当的时机主动放弃CPU的控制权,管理者不强制中断任务,调度这个动作是由多个任务主动协作来实现的。
抢占式调度初看起来非常的不讲理,如果某个线程马上就要执行完了确被强迫让出执行权,下次还要等待重新调度才能执行,这样会无端多出很多上下文切换的开销。但如果站在操作系统得视角,就能理解这样得无奈了,因为操作