前言
很多语言都实现了协程,比如
- python中的生成器(Generator)
- Lua中的Coroutine
- Go中的routine
- js中的async/await
本文主要讲解一些协程的基础知识,以及利用Kotlin手写实现python中的生成器,具体包括以下内容
1.什么是协程
2.协程的作用是什么
3.协程和线程的区别
4.协程的分类
5.python语言中的协程是怎样的
6.手写python协程实现
协程的基础知识
什么是协程
- 协程是可以由程序自行控制挂起,恢复的程序
- 协程可以用来实现多任务的协作执行
- 协程可以用来解决异步在任务控制流中的灵活转移
协程有什么作用
- 协程可以让异步代码同步化
- 协程可以降低异步代码的程序复杂度
- 同步代码比异步代码更灵活,更容易实现复杂业务
协程和线程的区别是什么?
线程是抢占式的,协程是协作式的
线程是根据时间片轮转,由操作系统调度
协程则是程序之间相互协作
如下图所示:
协程的分类
按调用栈
1.有栈协程:每个协程会分配单独的调用栈,类似线程的调用栈
2.无栈协程:不会分配单独的调用栈,挂起点状态通过闭包或对象保存
按调用关系
1.对称协程:调度权可以转移给任意协程,协程之间是对等关系
2.非对称协程:调度权只能转移给调用自己的协程,协程存在父子关系
python与kotlin协程都是无栈非对称协程
python协程是怎样的?
python语法中也存在协程,我们先介绍一下python中协程的简单用法
要理解协程,首先需要知道生成器是什么。生成器其实就是不断产出值的函数,只不过在函数中需要使用yield这一个关键词将值产出。
下面来看一个例子:
def gen():
n = 0
while True:
yield n
n += 1
g = gen()
print(g) # <generator object gen at 0x00000246E165A7C8>
print(next(g)) # 输出结果为0
print(next(g)) # 输出结果为1
复制代码
我们调用gen()函数并不会直接执行该函数,而是会得到一个生成器对象。对这个生成器对象调用next()函数,这个生成器对象会开始执行到第一个yield处,于是产出一个值0,注意:这时候gen()就暂停在yield处,直到第二次调用next()函数。
到这里我们可以发现,生成器函数是可以暂停的函数,它在调用方的驱使下(调用方使用next()函数),每次执行到yield处将yield后方的值产出给调用方后就暂停自己的执行,直到调用方下一次驱动它执行。
kotlin实现python协程
接下来看看我们怎么使用kotlin实现同样的效果
1.先看看最终要实现的调用代码
fun main() {
val nums = generator { start: Int ->
//限制yield的调用范围,只能在lambda中使用
for (i in 0..5) {
yield(start + i)
}
}
val seqs = nums(10)
for (j in seqs){
println(j)
}
}
复制代码
上面就是我们最终要实现的效果
可以看出,我们需要实现的有
1.generator方法
2.generator方法获得的结果需要支持Iterator接口
3.yield方法
4.yield需要限制在一定的范围之内
2.Generator接口与实现
定义Generator接口
interface Generator<T> {
operator fun iterator(): Iterator<T>
}
复制代码
定义Generator实现
class GeneratorImpl<T>(
private val block: suspend GeneratorScope<T>.(T) -> Unit,
private val parameter: T
) : Generator<T> {
override fun iterator(): Iterator<T> {
return GeneratorIterator(block, parameter)
}
}
复制代码
定义GeneratorScope
为了限定yield方法的使用范围,需要定义GeneratorScope
abstract class GeneratorScope<T> internal constructor() {
protected abstract val parameter: T
abstract suspend fun yield(value: T)
}
复制代码
写出generator方法
定义了上面这些内容,我们就可以写出generator方法了
fun <T> generator(block: suspend GeneratorScope<T>.(T) -> Unit): (T) -> Generator<T> {
return {
GeneratorImpl(block, it)
}
}
复制代码
上面的语句用到了带接收者的lambda表达式
lambda 作为形参函数声明时,可以携带接收者,如下图:
带接收者的 lambda 丰富了函数声明的信息,当传递该 lambda值时,将携带该接收者,比如:
// 声明接收者
fun kotlinDSL(block:StringBuilder.()->Unit){
block(StringBuilder("Kotlin"))
}
// 调用高阶函数
kotlinDSL {
// 这个 lambda 的接收者类型为StringBuilder
append(" DSL")
println(this)
}
>>> 输出 Kotlin DSL
复制代码
3.GeneratorIterator的具体实现
定义生成器状态
首先我们需要定义生成器的状态,方便后续判断生成器是否准备好,并根据相应条件做好状态转换
sealed class State {
class NotReady(val continuation: Continuation<Unit>) : State()
class Ready<T>(val continuation: Continuation<Unit>, val nextValue: T) : State()
object Done : State()
}
复制代码
如上所示,定义了3种状态
1.NotReady,未准备好,初始状态,挂起状态
2.Ready,已准备好,准备返回
3.Done,生成器内已经没有了数据
上面的语句使用了sealed密封类
1.密封(Sealed)类是一个限制类层次结构的类。
2.可以在类名之前使用sealed关键字将类声明为密封类。
3.它用于表示受限制的类层次结构。
4.当对象具有来自有限集的类型之一,但不能具有任何其他类型时,使用密封类。
5.密封类的构造函数在默认情况下是私有的,它也不能允许声明为非私有。
密封类的主要优点在于与when一起使用时
由于密封类的子类将自身类型作为一种情况。 因此,密封类中的when表达式涵盖所有情况,从而避免使用else子句。
具体实现
class GeneratorIterator<T>(
private val block: suspend GeneratorScope<T>.(T) -> Unit,
override val parameter: T
) : Iterator<T>, Continuation<Any?>, GeneratorScope<T>() {
override val context: CoroutineContext = EmptyCoroutineContext
private var state: State
init {
val coroutineBlock: suspend GeneratorScope<T>.() -> Unit = {
block(parameter)
}
val start = coroutineBlock.createCoroutine(this, this)
state = State.NotReady(start)
}
override fun hasNext(): Boolean {
resume()
return state != State.Done
}
override fun next(): T {
return when (val currentState = state) {
is State.NotReady -> {
return next()
}
is State.Ready<*> -> {
state = State.NotReady(currentState.continuation)
(currentState as State.Ready<T>).nextValue
}
State.Done -> throw IndexOutOfBoundsException("No Value Left")
}
}
override fun resumeWith(result: Result<Any?>) {
state = State.Done
result.getOrThrow()
}
override suspend fun yield(value: T) {
return suspendCoroutine {
state = when (state) {
is State.NotReady -> State.Ready(it, value)
is State.Ready<*> -> throw IllegalStateException("Cannot yield a value while ready")
State.Done -> throw IllegalStateException("Cannot yield a value while Done")
}
}
}
private fun resume() {
when (val currentState = state) {
is State.NotReady -> currentState.continuation.resume(Unit)
}
}
}
复制代码
主要的遍历,挂起与恢复逻辑便定义在GeneratorIterator中
1.调用coroutineBlock.createCoroutine创建协程
2.调用continuation.resume启动
3.调用传入的block,即yield,挂起函数,并更新状态
4.在next方法中返回值,并更新状态
5.然后再在hasNext方法中恢复协程,遍历到下一个
6.最后在完成时调用resumeWith,状态设置为Done
以上源码可见:kotlin手写python协程源码