• Kotlin协程:受限协程作用域与序列发生器


    一.受限协程作用域

        在Kotlin协程:协程的基础与使用中提到,可以通过sequence方法构建一个序列发生器。但当在sequence方法中调用除了yield方法与yieldAll方法以外的其他挂起方法时,就会报错。比如在sequence方法中调用delay方法,就会产生下面的报错提示:
    在这里插入图片描述
        翻译过来大致是“受限的挂起方法只能调用自身受限的协程作用域内的成员变量或挂起方法。这是什么意思呢?

    1. sequence方法

        sequence方法就是构建序列发生器用到的方法,内部通过Sequence方法实现,代码如下:

    @SinceKotlin("1.3")
    public fun <T> sequence(@BuilderInference block: suspend SequenceScope<T>.() -> Unit): Sequence<T> = Sequence { iterator(block) }
    
    • 1
    • 2

        其中参数block是一个在SequenceScope环境下的lambda表达式。

    2.SequenceScope类

    // 注意
    @RestrictsSuspension
    @SinceKotlin("1.3")
    public abstract class SequenceScope<in T> internal constructor() {
        // 向迭代器中提供一个数值
        public abstract suspend fun yield(value: T)
    
        // 向迭代器中提供一组数值
        public abstract suspend fun yieldAll(iterator: Iterator<T>)
    
        // 向迭代器中提供Collection类型的一组数值
        public suspend fun yieldAll(elements: Iterable<T>) {
            if (elements is Collection && elements.isEmpty()) return
            return yieldAll(elements.iterator())
        }
    
        // 向迭代器中提供Sequence类型的一组数值
        public suspend fun yieldAll(sequence: Sequence<T>) = yieldAll(sequence.iterator())
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

        SequenceScope类是一个独立的抽象类,没有继承任何的类。它提供了四个方法,只要都是用来向外提供数值或对象。而该类成为受限协程作用域的关键在于该类被RestrictsSuspension注解修饰,代码如下:

    @SinceKotlin("1.3")
    @Target(AnnotationTarget.CLASS)
    @Retention(AnnotationRetention.BINARY)
    public annotation class RestrictsSuspension
    
    • 1
    • 2
    • 3
    • 4

        RestrictsSuspension注解用于修饰一个类或接口,表示该类是受限的。在被该注解修饰的类的扩展挂起方法中,只能调用该注解修饰的类中定义的挂起方法,不能调用其他类的挂起方法。

        具体的,在sequence方法中,block就是SequenceScope类的扩展方法,因此在block中,只能使用SequenceScope类中提供的挂起方法——yield方法和yieldAll方法。同时,SequenceScope类的构造器被internal修饰,无法在外部被继承,因此也就无法定义其他的挂起方法。

        为什么受限协程作用域不允许调用其他的挂起方法呢?

        因为当一个方法挂起协程时,会获取协程的续体,同时协程需要等待方法执行完毕后的回调,这意味着会暴露协程的续体。可能会造成挂起协程执行的不确定性。

    二.序列发生器

    1.Sequence接口

        首先来分析一下Sequence接口,代码如下:

    public interface Sequence<out T> {
        public operator fun iterator(): Iterator<T>
    }
    
    • 1
    • 2
    • 3

    2.Sequence方法

        在协程中,有一个与Sequence接口同名的方法,该方法用于返回一个实现了Sequence接口的对象,代码如下:

    @kotlin.internal.InlineOnly
    public inline fun <T> Sequence(crossinline iterator: () -> Iterator<T>): Sequence<T> = object : Sequence<T> {
        override fun iterator(): Iterator<T> = iterator()
    }
    
    • 1
    • 2
    • 3
    • 4

        Sequence方法返回了一个匿名对象,并通过参数中的lambda表达式iterator实现了接口中的iterator方法。

        从sequence方法的代码可以知道,用于构建序列发生器的sequence方法内部调用了Sequence方法,同时还调用了iterator方法,将返回的Iterator对象,作为Sequence方法的参数。

    4. iterator方法

    @SinceKotlin("1.3")
    public fun <T> iterator(@BuilderInference block: suspend SequenceScope<T>.() -> Unit): Iterator<T> {
        val iterator = SequenceBuilderIterator<T>()
        iterator.nextStep = block.createCoroutineUnintercepted(receiver = iterator, completion = iterator)
        return iterator
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

        iterator方法内部创建了一个SequenceBuilderIterator对象,并且通过createCoroutineUnintercepted方法创建了一个协程,保存到了SequenceBuilderIterator对象的nextStep变量中。可以发现,序列发生器的核心实现都在SequenceBuilderIterator类中。

    5.SequenceBuilderIterator类

        SequenceBuilderIterator类是用于对序列发生器进行迭代,在该类的内部对状态进行了划分,代码如下:

    private typealias State = Int
    
    // 没有要发射的数据
    private const val State_NotReady: State = 0
    private const val State_ManyNotReady: State = 1
    // 有要发射的数据
    private const val State_ManyReady: State = 2
    private const val State_Ready: State = 3
    // 数据全部发射完毕
    private const val State_Done: State = 4
    // 发射过程中出错
    private const val State_Failed: State = 5
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

        状态转移图如下:
    在这里插入图片描述
        迭代器的初始状态为State_NotReady,由于首次发射没有数据,因此会进入State_Failed状态。

        State_Failed状态会从序列发生器中获取数据,如果是通过yield方法获取的数据,则会进入State_Ready状态,如果是通过yieldAll方法获取的数据,则会进入State_ManyReady状态。

        当从序列发生器中获取数据时,如果是在State_ManyReady和State_Ready状态,则直接发射一个数据,对应的进入到State_ManyNotReady和State_NotReady状态。如果是在State_ManyNotReady和State_NotReady状态,则会判断是否有数据,如果有数据则对应进入到State_ManyReady和State_Ready状态。如果没有则进入到State_Failed状态,获取数据。

        当序列发生器发射完毕时,会进入State_Done状态。

        接下来对SequenceBuilderIterator类进行分析。

    1)SequenceBuilderIterator类的全局变量

        SequenceBuilderIterator类继承自SequenceScope类,实现了Iterator接口和Continuation接口。代码如下:

    private class SequenceBuilderIterator<T> : SequenceScope<T>(), Iterator<T>, Continuation<Unit> {
        // 迭代器的状态
        private var state = State_NotReady
        // 迭代器下一个要发送的值
        private var nextValue: T? = null
        // 用于保存yieldAll方法传入的迭代器
        private var nextIterator: Iterator<T>? = null
        // 用于获取下一个数据的续体
        var nextStep: Continuation<Unit>? = null
        
        ...
        
        // 空的上下文
        override val context: CoroutineContext
            get() = EmptyCoroutineContext
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

        为什么SequenceBuilderIterator类的上下文是空的呢?

        因为SequenceBuilderIterator类继承了SequenceScope类,因此该类也是受限的,因此不允许在类的扩展方法中调用类内以外的挂起方法。自然也就不能进行调度、拦截等操作,所以上下文为空。在协程中,受限协程的上下文一般都是空上下文。

    2)yield方法与yieldAll方法

        yield方法与yieldAll方法是SequenceScope类中定义的两个方法,在SequenceBuilderIterator类中的实现如下:

    // 发射一个数据
    override suspend fun yield(value: T) {
        // 保存数据到全局变量中
        nextValue = value
        // 修改状态
        state = State_Ready
        // 挂起协程,获取续体
        return suspendCoroutineUninterceptedOrReturn { c ->
            // 保存续体到全局变量中
            nextStep = c
            // 挂起
            COROUTINE_SUSPENDED
        }
    }
    
    // 发射多个数据
    override suspend fun yieldAll(iterator: Iterator<T>) {
        // 如果迭代器没有数据,则直接返回
        if (!iterator.hasNext()) return
        // 如果有数据,则保存到全局变量
        nextIterator = iterator
        // 修改状态
        state = State_ManyReady
        // 挂起协程,获取续体
        return suspendCoroutineUninterceptedOrReturn { c ->
            // 保存续体到全局变量中
            nextStep = c
            // 挂起
            COROUTINE_SUSPENDED
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

        通过上面的代码可以知道,yield方法和yieldAll方法主要做了三件事情,挂起协程、修改状态、保存要发送的数据和续体。而yieldAll发射多个数据原理在于保存了参数中Iterator接口指向的对象,通过迭代器获取数据。

    3)hasNext方法

        hasNext方法是Iterator接口中定义的方法,用于迭代时判断是否还有数据,代码如下:

    override fun hasNext(): Boolean {
        // 循环
        while (true) {
            // 判断状态
            when (state) {
                // 刚通过yield方法发射数据
                State_NotReady -> {}
                // 刚通过yieldAll方法发射数据
                State_ManyNotReady ->
                    // 如果迭代器中还有数据
                    if (nextIterator!!.hasNext()) {
                        // 修改状态,返回true
                        state = State_ManyReady
                        return true
                    } else {
                        // 没有数据,则置空,丢弃迭代器
                        nextIterator = null
                    }
                // 如果序列发生器已经发射完数据,返回false
                State_Done -> return false
                // 如果有数据,则直接返回true
                State_Ready, State_ManyReady -> return true
                // 其他状态,则抛出异常
                else -> throw exceptionalState()
            }
            
            // 走到这里,说明需要去获取下一个数据
    
            // 修改状态
            state = State_Failed
            // 获取全局保存的续体
            val step = nextStep!!
            // 置空
            nextStep = null
            // 恢复序列发生器的执行,直到遇到yield方法或yieldAll方法挂起
            step.resume(Unit)
        }
    }
    
    // 异常状态的处理
    private fun exceptionalState(): Throwable = when (state) {
        State_Done -> NoSuchElementException()
        State_Failed -> IllegalStateException("Iterator has failed.")
        else -> IllegalStateException("Unexpected state of the iterator: $state")
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45

    4)next方法

        next方法也是Iterator接口中定义的方法,用于在迭代器中存在数据时获取数据,代码如下:

    override fun next(): T {
        // 判断状态
        when (state) {
            // 如果当前处于已经发射完数据的状态,则判断是否有数据
            State_NotReady, State_ManyNotReady -> return nextNotReady()
            // 如果通过yieldAll方法获取到了数据
            State_ManyReady -> {
                // 修改状态
                state = State_ManyNotReady
                // 通过迭代器获取数据
                return nextIterator!!.next()
            }
            // 如果通过yield方法获取到了数据
            State_Ready -> {
                // 修改状态
                state = State_NotReady
                // 获取保存的数据并进行类型转换
                @Suppress("UNCHECKED_CAST")
                val result = nextValue as T
                // 全局变量置空
                nextValue = null
                // 返回数据
                return result
            }
            // 其他情况,则抛出异常
            else -> throw exceptionalState()
        }
    }
    
    // 如果没有数据,则抛出异常,有数据,则返回数据
    private fun nextNotReady(): T {
        if (!hasNext()) throw NoSuchElementException() else return next()
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    6.总结

        当使用序列发生器进行迭代时,首先会调用hasNext方法,hasNext方法会通过保存的续体,恢复序列发生器所在的协程继续执行,获取下一次待发射的数据。如果获取了到数据,则会返回true,这样之后通过next方法就可以获取到对应的数据。

        当序列发生器所在的协程在执行中遇到yield方法时,会发生挂起,同时将下一次待发射的数据保存起来。如果遇到的是yieldAll方法,则保存的是迭代器,下一次发射数据时会从迭代器中获取。

  • 相关阅读:
    MemFire Cloud: 一种全新定义后端即服务的解决方案
    UnityAPI学习之 播放游戏音频的类(AudioSource)
    论文浅尝 | 深度神经网络的模型压缩
    Android 预置应用到系统内
    github push 失败 git did not exit cleanly(exit code 128) 账号登录失败 解决经验
    【分布式技术专题】「架构实践于案例分析」总结和盘点目前常用分布式事务特别及问题分析(上)
    XTTS系列之二:不可忽略的BCT
    4位资深专家多年大厂经验分享出Flink技术架构设计与实现原理
    接口测试经验分享
    图书馆座位预约系统管理/基于微信小程序的图书馆座位预约系统
  • 原文地址:https://blog.csdn.net/LeeDuoZuiShuai/article/details/126323313