概述
笔者之前已经对进程和线程有过了一定的探索,有兴趣的读者可以了解下:
近期笔者又接触到了协程,顺便也想尝试用一种更加通俗易懂的方式讲下这几个概念。
期望能对有需要的读者有所帮助,如有理解不对的地方欢迎评论指出。
为什么要有多进程?
多进程目的:提高cpu的使用率
例子:一个用户现在既想使用打印机,又想玩游戏。
假设只有一个进程(先不谈多线程)
从操作系统的层面看,我们使用打印机的步骤有如下:
- 使用CPU执行程序,去硬盘读取需要打印的文件,然后CPU会长时间的等待,直到硬盘读写完成。
- 使用CPU执行程序,让打印机打印这些内容,然后CPU会长时间的等待,等待打印结束。
在这样的情况下,其实CPU的使用率其实非常的低。
打印一个文件从头到尾需要的时间可能是1分钟,而cpu使用的时间总和可能加起来只有几秒钟。
而后面如果单进程执行游戏的程序的时候,CPU也同样会有大量的空闲时间。
使用多进程后
当CPU在等待硬盘读写文件,或者在等待打印机打印的时候,CPU可以去执行游戏的程序,这样CPU就能尽可能高的提高使用率。
再具体一点说,其实也提高了效率。因为在等待打印机的时候,这时候显卡也是闲置的,如果用多进程并行的话,游戏进程完全可以并行使用显卡,并且与打印机之间也不会互相影响。
有了进程,为啥还要有线程?
多进程的引入提高了CPU的使用率,但是同时也会引入一些新的问题:
- 多进程之间的资源不共享。
游戏进程无法直接访问打印文件进程的资源。 - 多进程之间如果需要互相访问资源,那么就需要"进程间通信(简称IPC)"——这是需要消耗一定资源的,比如:维护临时变量,实现进程间同步等。
对IPC有兴趣的读者可以看下笔者的另一篇文章:
linux进程间通信(IPC)小结
在很多场景下,其实用户是期望资源本身就可以共享的。
比如:
- 我们玩游戏的时候,期望一边能继续玩,一边能后台加载后续的内容。
- 我们在使用视频软件的时候,我们期望视频能一边下载,一边播放已经下载完的内容。
线程之间的资源可以共享,在这些场景下使用就非常合适。
补充说明:在linux操作系统中,”线程“也称为”轻量级进程“
有了线程,为啥还要有协程?
如今出现了一种场景:
开发者在每个线程中只做非常轻量的操作,比如访问一个极小的文件,下载一张极小的图片,加载一段极小的文本等。但是,这样”轻量的操作“的量却非常多。
在有大量这样的轻量操作的场景下,即使可以通过使用线程池来避免创建与销毁的开销,但是线程切换的开销也会非常大,甚至于接近操作本身的开销。
对于这些场景,就非常需要一种可以减少这些开销的方式。
于是,协程就应景而出,非常适合这样的场景。
协程如何能减少开销
线程切换的开销主要有以下两点:
- 恢复现场成本。
cpu缓存中的资源需要从一个线程更新到另一个线程。(在java中,这块内容也称为工作区) - 保护现场成本。
操作系统切换线程需要使用系统中断,保留现场也需要消耗一定资源。
使用协程,虽然也不可能完全避免这两点开销,但是由于协程不再是交给操作系统控制的,而是直接由开发者在用户空间控制。
因此开发者完全就可以通过一系列更加定制化的操作来减少某些场景下的开销了。
由于协程开发者直接在用户空间控制,因此用户完全可以自己开发出一套逻辑来部分模拟“线程切换”的操作。
这样虽然需要自己手动维护这样的逻辑,也有一定的开销,但是却能避免操作系统线程切换的开销。
优势
由于不需要操作系统干预,因此所有操作完全可以在用户空间实现,更加轻量。
以下内容引用自《深入理解Java虚拟机》:
一个线程的栈通常在几百个字节到几KB之间,所以Java虚拟机里线程池容量达到两百就已经不算小了,而很多支持协程的应用中,同时并存的协程数量可数以十万计。
劣势
所有原先作用于线程的操作,用户都需要自己去处理,如:协程的创建、销毁、切换、调度等。
以下内容引用自《深入理解Java虚拟机》:
具体到Java语言,还会有一些别的限制,譬如HotSpot这样的虚拟机,Java调用栈跟本地调用栈是做在一起的。如果在协程中调用了本地方法,还能否正常切换协程而不影响整个线程?另外,如果协程中遇传统的线程同步措施会怎样?譬如Kotlin提供的协程实现,一旦遭遇synchronize关键字,那挂起来的仍将是整个线程。
线程-协程 知识深化
《深入理解Java虚拟机》中针对”协程的复苏“的理解如下:
内核线程的调度成本主要来自于用户态与核心态之间的状态转换,而这两种状态转换的开销主要来自于响应中断、保护和恢复执行现场的成本。请读者试想以下场景,假设发生了这样一次线程切换:
线程A-> 系统中断 ->线程B
处理器要去执行线程A的程序代码时,并不是仅有代码程序就能跑得起来,程序是数据与代码的组合体,代码执行时还必须要有上下文数据的支撑。而这里说的“上下文”,以程序员的角度来看,是方法调用过程中的各种局部的变量与资源;以线程的角度来看,是方法的调用栈中存储的各类信息;而以操作系统和硬件的角度来看,则是存储在内存、缓存和寄存器中的一个个具体数值。物理硬件的各种存储设备和寄存器是被操作系统内所有线程共享的资源,当中断发生,从线程A切换到线程B去执行之前,操作系统首先要把线程A的上下文数据妥善保管好,然后把寄存器、内存分页等恢复到线程B挂起时候的状态,这样线程B被重新激活后才能仿佛从来没有被挂起过。这种保护和恢复现场的工作,免不了涉及一系列数据在各种寄存器、缓存中的来回拷贝,当然不可能是一种轻量级的操作。