阅读 877

【手写协程】 实现kotlin版yield与resume

前言

yield(挂起)与resume(恢复)是一种常见的协程实现,例如在Lua语言中的协程就是这样实现的
但是在kotlin中并没有这种语法,而是直接的launch
本文主要通过手写实现kotlin版的yield与resume
有助于读者更加深入地理解协程挂起与恢复的原理

Lua协程是怎么使用的?

1.coroutine.create() 创建 coroutine,返回 coroutine, 参数是一个函数,当和 resume 配合使用的时候就唤醒函数调用
2.coroutine.resume() 重启 coroutine(重启时不用再传参数),和 create 配合使用
3.coroutine.yield() 挂起 coroutine,将 coroutine 设置为挂起状态,这个和 resume 配合使用能有很多有用的效果(返回参数)

我们要达成的效果

我们将利用kotlin协程api,实现Lua协程效果,并实现一个生产者消费者的demo
最后调用如下:

suspend fun main() {
    val producer = Coroutine.create<Unit, Int>(Dispatcher()) {
        for (i in 0..3) {
            log("send", i)
            yield(i)
        }
        200
    }

    val consumer = Coroutine.create<Int, Unit>(Dispatcher()) { param: Int ->
        log("start", param)
        for (i in 0..3) {
            val value = yield(Unit)
            log("receive", value)
        }
    }

    while (producer.isActive && consumer.isActive){
        val result = producer.resume(Unit)
        consumer.resume(result)
    }
}
复制代码

如上所示,为了实现如上效果,我们需要实现
1.线程调度器Dispatcher
2.Coroutine.create方法
3.协程内调用yield挂起方法
4.isActive状态判断方法
5.resume恢复方法

1.首先看看create方法

fun <P, R> create(
            context: CoroutineContext = EmptyCoroutineContext,
            block: suspend Coroutine<P,R>.CoroutineBody.(P) -> R
        ): Coroutine<P, R> {
            return Coroutine(context, block)
        }

private val body = CoroutineBody()

private val status: AtomicReference<Status>

val isActive: Boolean
    get() = status.get() != Status.Dead

init {
    val coroutineBlock: suspend CoroutineBody.() -> R = { block(parameter!!) }
    val start = coroutineBlock.createCoroutine(body, this)
    status = AtomicReference(Status.Created(start))
}
复制代码

创建时主要做了以下工作
1.创建协程体并初始化
2.初始化协程状态
3.添加协程isActive方法

2.协程的几种状态

通过密封类定义了如下几种状态

sealed class Status {
    class Created(val continuation: Continuation<Unit>): Status()
    class Yielded<P>(val continuation: Continuation<P>): Status()
    class Resumed<R>(val continuation: Continuation<R>): Status()
    object Dead: Status()
}
复制代码

可以注意到每个状态内都有一个Continuation接口,可以获取到协程上下文

3.resume实现

suspend fun resume(value: P): R = suspendCoroutine { continuation ->
        val previousStatus = status.getAndUpdate {
            when(it) {
                is Status.Created -> {
                    body.parameter = value
                    Status.Resumed(continuation)
                }
                is Status.Yielded<*> -> {
                    Status.Resumed(continuation)
                }
                is Status.Resumed<*> -> throw IllegalStateException("Already resumed!")
                Status.Dead -> throw IllegalStateException("Already dead!")
            }
        }

        when(previousStatus){
            is Status.Created -> previousStatus.continuation.resume(Unit)
            is Status.Yielded<*> -> (previousStatus as Status.Yielded<P>).continuation.resume(value)
        }
    }
复制代码

协程主要是通过状态机实现的,挂起与恢复就是状态的流转
当我们resume时,首先判断协程的state并切换
同时如果state是create或Yielded,则恢复上一个挂起点

AtomicReference

status是AtomicReference类型的
AtomicReference类提供了一个可以原子读写的对象引用变量。 原子意味着尝试更改相同AtomicReference的多个线程(例如,使用比较和交换操作)不会使AtomicReference最终达到不一致的状态。 AtomicReference甚至有一个先进的compareAndSet()方法,它可以将引用与预期值(引用)进行比较,如果它们相等,则在AtomicReference对象内设置一个新的引用。

4.yield实现

inner class CoroutineBody {
        var parameter: P? = null

        suspend fun yield(value: R): P = suspendCoroutine { continuation ->
            val previousStatus = status.getAndUpdate {
                when(it) {
                    is Status.Created -> throw IllegalStateException("Never started!")
                    is Status.Yielded<*> -> throw IllegalStateException("Already yielded!")
                    is Status.Resumed<*> -> Status.Yielded(continuation)
                    Status.Dead -> throw IllegalStateException("Already dead!")
                }
            }

            (previousStatus as? Status.Resumed<R>)?.continuation?.resume(value)
        }
    }
复制代码

yield方法同样是状态的流转
切换状态后,如果之前的状态是resume,则恢复上一个挂起点

5.拦截器切换线程

我们可以利用协程拦截器切换线程,这样就可以在任意线程调用我们写的yeild与resume了

class Dispatcher: ContinuationInterceptor {
    override val key = ContinuationInterceptor

    private val executor = Executors.newSingleThreadExecutor()

    override fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T> {
        return DispatcherContinuation(continuation, executor)
    }
}

class DispatcherContinuation<T>(val continuation: Continuation<T>, val executor: Executor): Continuation<T> by continuation {

    override fun resumeWith(result: Result<T>) {
        executor.execute {
            continuation.resumeWith(result)
        }
    }
}
复制代码

添加拦截器后,协程会在我们创建的线程池中运行

总结

运行效果

00:01:41:906 [pool-1-thread-1] send 0
00:01:41:948 [pool-2-thread-1] start 0
00:01:41:948 [pool-1-thread-1] send 1
00:01:41:948 [pool-2-thread-1] receive 1
00:01:41:948 [pool-1-thread-1] send 2
00:01:41:949 [pool-2-thread-1] receive 2
00:01:41:949 [pool-1-thread-1] send 3
00:01:41:949 [pool-2-thread-1] receive 3
00:01:41:950 [pool-2-thread-1] receive 200
复制代码

最后调用效果如上,实现了自己手写一个简单的resume与yield,并实现了生产者与消费者模式
相信读完本文,读者可以更好的理解协程的挂起与恢复是怎样实现的

本文所有相关代码

本文所有相关代码