- 线程
- 回调
- futures、 Promise s
- 响应式扩展
- 协程
1. 线程
假如有一个任务需要长时间运行,而且会阻塞用户界面,我们可以在一个单独的线程中运行这个线程,避免阻塞UI,但是存在很多缺点:
- 线程有昂贵的上下文切换;
- 线程数量受底层操作系统的限制;
- 线程并不总是可用,如在JS中不支持线程;
- 线程不容易使用,线程的Debug、竞态条件是多线程中常见的问题
2. 回调
将一个函数作为参数传递给另一个函数,处理完成后调用此函数,但是会存在以下问题:
- 回调嵌套的难度。通常被用作回调的函数,经常最终需要自己的回调。导致一些列回调嵌套并导致出现难以理解的代码;
- 错误处理很复杂。嵌套模型使错误处理和传播更加复杂。
3. Futures,Promises
发起调用的时候,在某些时候将它返回一个Promise的可被操作的对象;对 异步调用 结果的一个封装;
4. 响应式扩展
Rx背后的想法是走向所谓的可观察流,将数据视为流,并且可观察;Rx很简单,Observer Pattern带有一系列扩展,允许对数据进行操作; 方法上与Futures非常相似,但是人们可以将Future视为一个离散元素,而Rx返回一个流 因此提出了一种新的编程模型方式: 一切都是流,并且是可观察的。
5. CPS变换 Coroutine与async/await
无论是响应式还是Promise,仍然没有摆脱手工构造Continuation,开发者要把业务逻辑改写为 回调函数 ;对于线性逻辑基本可以应付自如,如果逻辑复杂一点呢?
- O -> A -> B -> Z (promise is ok)
- O -> A ( B or C ) -> Z(Rx is Ok)
- O -> A -> B -> (C->A or Z) how to solve it?
在C#、JS、py中提供了async/await,摆脱了回调函数,在异步函数调用时加上await, 编译器会自动将它转为协程
CPS变换:把普通函数转成一个CPS函数,即Continuation也能作为一个调用参数。函数不仅能从头运行,还能根据Continuation的指示继续从某个点运行,比如IO调用的地方。
此时函数变成了一个 状态机 ,每次调用或者调用其他异步函数,状态机都会做一些计算和状态转换。Kotlin中的实现会将Continuation编译为一个新的对象。 jvm 上有一个实现,在Bytecode中实现CPS变换 ea-async
6. 用户态线程
有了async/await后,易用性有了很大提升。更彻底的实现是用户态线程,代表的有golang、jvm中的quasar;用户态线程把操作系统提供的线程机制完全抛弃,不去用这个VM的虚拟化机制。比如硬件有16个线程,就把若干用户态线程调度到16个系统线程上运行;同时所有可能阻塞系统级线程的如sleep、recv,会阻塞对应的系统线程。go runtime接管这样的系统调用,用统一的event loop来调度。
7. 小结
以上方案中,Promise、Reactive本质还是回调函数,只是提升了易用性;async/await和用户态线程是更彻底的实现,前者通过编译器的CPS变换创造出CPS式的函数调用;后者绕开操作系统,重新实现一套线程和调度机制。