关于异步编程的一点思考
18 Oct 2024异步编程理解起来没那么符合直觉,不过抓住两个点就好了:
- 不管语言层面对异步编程怎么包装,本质都是为了让你以同步的方式来写代码
- 计算机世界就是一个大 loop,所以异步编程的实现上一定有回调函数
第一点理解起来比较容易,第二点没那么直接,我们看几个常见的关于异步编程的实现就理解了。
ReactiveX
ReactiveX 框架是为了解决使用回调函数的时候出现多层嵌套的 callback 结构,aka the Callback Hell。
ReactiveX 是通过使用 观察者 设计模式来解决这个问题的,把嵌套的结构变成了级联的结构。具体的原理推荐看 NotRxJava guide for lazy folks 这篇文章。
这里只是用设计模式来对 callback 进行了简单的封装。
Node.js
JS 里最开始在优化 callback 的时候搞出了 Promise,这个和 ReactiveX 一样也是通过 观察者模式 来实现的。
后来 JS 又搞出了 generator 函数,配合 yield 和 执行器 就可以在开发的时候比较接近同步编程了。后面更近一步的提供了 async 和 await 这两个语法糖关键字,但本质上还是 generator 那套实现。具体的原理推荐看 阮一峰 的 深入掌握 ECMAScript 6 异步编程 这系列文章,来龙去脉讲的很清楚。
这里的回调函数在哪里呢?回调函数在执行器里,yield 返回 Promise 对象,执行器里传入一个回调函数给 Promise,这个回调函数实现上用了递归,这样可以实现 generator 函数的自动执行。
Kotlin
Kotlin 的协程实现理解上比较绕,我觉得原因在于它是在 JVM 上用语法层面的东西来模拟协程的。你写的 suspend 函数会被 Kotlin 的编译器转换成一个叫 Continuation 的东西,这玩意本质上就是一个回调函数。你可以理解成你在 suspend 函数里每一次对另外一个 suspend 的函数调用其实就是在 Continuation 里增加了一个回调点。实现上,Kotlin 会在每次调用 suspend 函数的时候,把调用方的 suspend 的函数对应的 Continuation 当作回调函数给到被调用方的 suspend 函数。推荐看 破解 Kotlin 协程(6):协程挂起篇 这篇文章,文章最后有个示例代码,基本就是 Kotlin 协程运行时的本质了。
Go
go 在官方文档里推荐我们在并发编程的时候:
Do not communicate by sharing memory; instead, share memory by communicating.
这样我们在处理并发任务时编写的代码就是同步的了。但这导致程序运行时有大量阻塞操作,我们知道,在绝大多数语言里,线程是比较昂贵的资源,所以 go 用 goroutine 替代了 线程。实现机制上 goroutine 做阻塞操作的时候调度器会把 goroutine 放到一个等待队列里,等到时机合适时通过回调的形式来恢复执行。