本人没有多少js与python、kotlin的开发经验,纯粹逻辑推理+脑洞,有偏颇之处还请不吝赐教。
进程、线程、纤程、协程
- 程序的本质无非是计算与状态,反映到cpu就是计算单元与寄存器。当然,寄存器太小,不够容纳所有状态,所以就需要cpu缓存、内存、乃至硬盘(不作为输入,只作为存储介质)等,但这些东西本质上还是个大号的寄存器,如果寄存器容量无限的话,根本不需要这些。
- 程序运行过程中,有时候需要等待某些条件满足才能继续运行,为了提高cpu利用率,在这种时候,我们可以将程序的状态记录下来,切换到其他程序运行,等到等待的条件满足了,再切换回来,并恢复之前的状态继续运行。
- 程序运行过程中的状态主要存储在内存中,为了降低切换的开销,我们不释放内存资源(如果内存不足,可能会swap,但是生产中是不允许swap的),并将寄存器中的数据也存储在内存中。切换回来后,将数据恢复到寄存器,从之前暂停的地方继续运行。
- 看起来一切都很完美,直到所有程序都在等待资源。这时候,你会想,真的没有可以让cpu 干的活了吗?答案通常是否定的,每个程序里肯定还有一些可以不依赖正在等待的资源的工作。那么怎么去执行这部分任务呢?很简单,允许程序里“嵌套”程序,让“嵌套”程序去执行这些任务。最外面的程序我们通常叫“进程”,被“嵌套”的程序叫“线程”。注意,无论是”进程“还是”线程“,它们所代表的仅仅是操作系统管理计算资源的一种方式而已,它们不是计算资源本身,它们是一种数据结构,保存着程序运行的状态。包括纤程与协程,都应该这样去理解。
- 由于”线程“是”嵌套“在”进程“中的,所以同一进程中的线程通常与其进程共享某些资源,比如内存。所以进程还充当了管理者的角色,真正执行任务的变成了线程(可以把进程也理解为一种特殊的线程)。线程运行过程中,自然也有很多状态,而这些状态与3中描述的并无区别。
- 这下应该完美了,所有的程序都可以通过线程无限并发,把cpu榨干。但很不幸,问题恰恰在于,我们把cpu“榨”的太干了。我们知道,从一个线程切换到另一个线程需要保存状态,切换上下文,而这个开销在某些情况下是不容忽视的。如果我们在程序中仅仅运行几个指令就需要等待某种资源,然后切换到其他线程,在这种情况下,线程切换所耗费的资源已经远大于程序运行所耗费的资源了,很显然造成了cpu资源的浪费。为了缓解这种情况,我们可以将线程的切换在用户态完成,这就是纤程。操作系统的线程调度通常是抢占式的,而纤程通常是协作式的(来自维基百科,其实我觉得这不重要,它的特点更重要的是“用户线程”)。纤程通常实现为有栈的协程,或者叫协作式的用户线程。
- 协程是什么?我的理解是协程是自由的控制流。现在的编程语言,通常是通过方法调用去实现流程控制的,方法调用有调用者与被调用者的区别,而且方法执行完成后控制权必须返回调用者;而协程间的相互“调用”是“对称”的,方法调用可以认为是协程的一种特例。协程与方法的关系就如同jmp与call的关系,前者是更本质的东西,后者只是在前者的基础上进行了一些定制化包装。
- 协程如何实现?想要实现控制权的自由转换,并不是一件容易的事,它意味着必须管理好每个协程的状态。目前市面上的协程实现五花八门,这里举几个比较热门的语言中的协程实现。
-
js与python中的async/await都是Function对象或者lambda函数的语法糖,本质上还是方法调用,只是将以前需要写的callback换成了await,提高了代码可读性,性能方面并无提高。
-
kotlin与js/python类似,主要还是利用lambda函数调用,但是支持在某些特殊情况下,将协程在线程间转移(本质就是多线程模型,需要执行的任务没有上下文依赖等限制),可以理解是将单线程模型下的协程与多线程模型结合起来了。性能上相对于多线程模型也没有啥优势。
-
goroutine可能更应该称作用户线程,因为已经支持抢占了,而且是有栈的,这意味着可以从任意协程切换到其他任意协程,这使得在多处理器间调度成为可能。
Revisiting Coroutines[5] published in 2009 proposed term Full Coroutine to denote one that supports first-class coroutine and is stackful.
”goroutine“才是“full coroutine”。java的virtual thread也是与goroutine类似的实现,也是”full coroutine”。这种协程其实更像是纤程。由于协程调度在用户态,开销比系统的线程调度小得多,所以对于那些可能会频繁切换线程的系统,使用协程可以大幅提高性能。比如在Javaweb领域,主要使用阻塞的servlet模型,每个servlet需要绑定一个线程,所以在用户请求很多的情况下,会有很多的cpu资源用来切换线程了。如果用协程模型,这些切换只需要在用户态即可完成,大大降低了资源消耗。进一步的,将类似功能的协程运行在相同的线程上,还可以提高缓存命中率。
-