前言
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,并实现了生产者与消费者模式
相信读完本文,读者可以更好的理解协程的挂起与恢复是怎样实现的