UPDATING...
异步与协程
异步计算的两个基本原语可以归结为暂停和继续,所以所有的异步计算模式都是围绕着如何暂停一个任务和如何继续已暂停的任务两个问题来的。
过程和挂起点
异步计算的两个基本元素是过程和挂起点。过程指的是一个任务的执行流程,体现在编程中就是完成某个任务的代码的集合。过程中可以有挂起点,指的是过程在执行时可以使其自身暂停运行的点。不可中断过程是指不包含挂起点的过程。一个过程往往是由一系列的挂起点和不可中断过程按顺序组合起来的,不可中断过程的前后一定是挂起点。
过程并不与函数对应,它所表达的是一个任务,理解这一点是至关重要的。当我们在描述一个任务是异步的时,只是强调这个任务是可以被分为多个步骤来执行的,并未规定这个任务看起来的表象是如何的,所以上述我对“过程”的定义只是“一些代码的集合”,至于这些代码如何组织,放到多个函数还是一个函数中之类的问题,就是各种异步模式所考虑的事情了。
挂起点拥有挂起过程的能力,但并不是说过程运行到挂起点就一定会被挂起,只有该挂起点被接受了,过程才会挂起。不同的异步模式会有不同的挂起方法,也就是创建挂起点的手段:
异步模式 | 挂起原语 | 实现例子 |
---|---|---|
async/await | await | python asyncio(v3.5+), C#, js |
生成器 | yield | python asyncio(v3.4) |
事件循环+回调 | 调用拥有回调的函数 | js |
future | 设置future的回调 | java concurrent |
其中,前两者是基于等待的挂起,后两者是基于回调的挂起,这是两种截然不同的过程组织方式。
挂起的时机
在语言级别的异步计算的中,一个过程的挂起点往往是有限的,一般只在与IO等待、定时器等待、用户输入等待等相关的地方可挂起。而在操作系统的线程调度中,线程的每条汇编都是一个不可中断过程,每条汇编之间都是挂起点,但是线程的挂起点只有少数会被接受。
过程之间的等待
另外,要实现一个可用的异步计算框架,异步过程之间的嵌套调用机制必不可少。过程的嵌套调用本质上是对多个挂起点的归一化。
之所以状态机经常会成为实现协程的方案,一是因为状态机能够很好地表达挂起和继续,二是状态机拥有表达协程所需要的多次平级挂起的能力。
状态机的互相等待。
过程序列的线性表达
协程是对可分过程序列的线性表达,因而只要可以实现过程、线性序列两个功能的都可以实现协程。函数模型本就是一种可分过程的线性模型,其底层的指令集也是一样,又比如Promise[]、Asyncify。
异步的优势
单独一个任务没必要讲异步不异步,只有当多个任务并发执行时,异步任务才能体现出优势:计算资源不必阻塞于等待某个任务的某个资源的就绪,而是可以转而执行另外的任务,减少了计算资源的阻塞。
这个优势可以从不同角度来理解:
- 从任务的角度:任务不需要一直占据计算资源,只是在必要的时候被执行,因而可以同时执行多个任务;
- 从资源的角度:因为可能有多个任务被同时挂起,计算资源得以同时等待多个耗时资源。