【手写协程】怎么利用kotlin实现python协程?

【手写协程】怎么利用kotlin实现python协程?

Android小彩虹2021-08-17 7:41:12190A+A-

前言

很多语言都实现了协程,比如

  • 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协程源码

参考资料

Python协程
带接收者的 lambda表达式
Kotlin Sealed类

点击这里复制本文地址 以上内容由权冠洲的博客整理呈现,请务必在转载分享时注明本文地址!如对内容有疑问,请联系我们,谢谢!

支持Ctrl+Enter提交

联系我们| 本站介绍| 留言建议 | 交换友链 | 域名展示
本站资源来自互联网收集,仅供用于学习和交流,请遵循相关法律法规,本站一切资源不代表本站立场,如有侵权、后门、不妥请联系本站删除

权冠洲的博客 © All Rights Reserved.  Copyright quanguanzhou.top All Rights Reserved
苏公网安备 32030302000848号   苏ICP备20033101号-1
本网站由 提供CDN/云存储服务

联系我们