一文带你理解Kotlin协程本质核心

·  阅读 1053

1. 协程是什么

  • 协程是编译器的能力,因为协程并不需要操作系统和硬件的支持(线程需要),是编译器为了让开发者写代码更简单方便, 提供了一些关键字, 并在内部自动生成了处理字节码

线程和协程的目的差异

  • 线程的目的是提高CPU资源使用率, 使多个任务得以并行的运行,是为了服务于机器的.
  • 协程的目的是为了让多个任务之间更好的协作,主要体现在代码逻辑上,是为了服务开发者 (能提升资源的利用率, 但并不是原始目的)

线程和协程的调度差异

  • 线程的调度是系统完成的,一般是抢占式的,根据优先级来分配
  • 协程的调度是开发者根据程序逻辑指定好的,在不同的时期把资源合理的分配给不同的任务.

协程与线程的关系

  • 协程并不是取代线程,而且抽象于线程之上,线程是被分割的CPU资源,协程是组织好的代码流程,协程需要线程来承载运行,线程是协程的资源

2. 基本使用

2.1. CoroutineScope.launch

  • launch函数可以启动新协程而不将结果返回给调用方

2.1.1. 代码实现

//获取一个协程作用域用于创建协程
private val mScope = MainScope()

mScope.launch(Dispatchers.IO) {
            //IO线程执行getStringInfo()方法,返回结果
            var res = getStringInfo()
            //获取结果后主线程提示更新
            withContext(Dispatchers.Main) {
                Alerter.create(this@LearnCoroutineActivity).setTitle("Result").setText(res).show()
            }
}

private suspend fun getStringInfo(): String {
        return withContext(Dispatchers.IO) {
            //在这1000毫秒内该协程所处的线程不会阻塞
            delay(1000)
            "Coroutine-launch"
        }
}
    
//在onDestroy生命周期方法之中要手动取消
override fun onDestroy() {
        super.onDestroy()
        mScope.cancel()
}
复制代码

2.1.2. 步骤

  1. 获取一个协程作用域用于创建协程
  2. 通过协程作用域.launch方法启动新的协程任务
    1. 启动时可以指定执行线程
    2. 内部通过withContext()方法实现切换线程
  3. 在onDestroy生命周期方法之中要手动取消

2.2. CoroutineScope.async

  • async函数实现返回值处理或者并发处理

2.2.1. 返回值处理

private fun asyncReturn() {
        mScope.launch(Dispatchers.Main) {
            //新开一个协程去执行协程体,父协程的代码会接着往下走
            var deferred = async(Dispatchers.IO) {
                delay(1000)
                "Coroutine-Async"
            }
            //等待async执行完成获取返回值,并不会阻塞线程,而是挂起,将线程的执行权交出去
            //直到async的协程体执行完毕后,会恢复协程继续执行
            val data = deferred.await()
            Alerter.create(this@LearnCoroutineActivity).setTitle("Result").setText(data).show()
        }
    }
复制代码

2.2.2. 并发处理

private fun asyncConcurrent() {
    	//coroutineContext的创建下文会有分析
        var coroutineContext = Job() +
                Dispatchers.Main +
                CoroutineExceptionHandler { coroutineContext, throwable ->
                    Log.e(
                        "CoroutineException",
                        "CoroutineExceptionHandler: $throwable"
                    )
                } +
                CoroutineName("asyncConcurrent")
        mScope.launch(coroutineContext) {
            val job1 = async(Dispatchers.IO) {
                delay(1000)
                "job1-finish"
            }
            val job2 = async(Dispatchers.IO) {
                delay(2000)
                "job2-finish"
            }
            val job3 = async(Dispatchers.IO) {
                delay(500)
                "job3-finish"
            }
            //等待各job执行完 将结果合并
            Alerter.create(this@LearnCoroutineActivity).setTitle("Result")
                .setText("job1:${job1.await()},job2:${job2.await()},job3:${job3.await()}").show()
        }
    }
复制代码

2.3. 协程作用域

  • MainScope是协程默认提供的作用域,但是还有其他作用域更为方便
  • 可使用lifecycleScope或者viewModelScope,这两种作用域会自动取消
  • 在UI组件中使用LifecycleOwner.lifecycleScope,在ViewModel中使用ViewModel.viewModelScope

3. CoroutineContext

  • CoroutineContext是一个特殊的集合,同时包含了Map和Set的特点

  • 集合内部的元素Element是根据key去对应(Map特点),但是不允许重复(Set特点)

  • Element之间可以通过+号进行组合

  • Element有如下四类,共同组成了CoroutineContext

    • Job:协程的唯一标识,用来控制协程的生命周期(new、active、completing、completed、cancelling、cancelled)
    • CoroutineDispatcher:指定协程运行的线程(IO、Default、Main、Unconfined)
    • CoroutineName: 指定协程的名称,默认为coroutine
    • CoroutineExceptionHandler: 指定协程的异常处理器,用来处理未捕获的异常

3.1. CoroutineDispatcher Element

  • 用于指定协程的运行线程
  • kotlin已经内置了CoroutineDispatcher的4个实现,可以通过Dispatchers的Default、IO、Main、Unconfined字段分别返回使用
public actual object Dispatchers {
    @JvmStatic
    public actual val Default: CoroutineDispatcher = createDefaultDispatcher()
    @JvmStatic
    public val IO: CoroutineDispatcher = DefaultScheduler.IO
    @JvmStatic
    public actual val Unconfined: CoroutineDispatcher = kotlinx.coroutines.Unconfined
    @JvmStatic
    public actual val Main: MainCoroutineDispatcher get() = MainDispatcherLoader.dispatcher
}
复制代码

3.1.1. Default,IO

Default,IO其实内部用的是一个线程池,下面逐个解析,看实现原理

3.1.1.1. default
  • Default会根据useCoroutinesScheduler属性(默认为true)去获取对应的线程池
    • DefaultScheduler(useCoroutinesScheduler=ture):kotlin自己实现的线程池逻辑
    • CommonPool(useCoroutinesScheduler=false):java类库中的Executor实现线程池逻辑
internal actual fun createDefaultDispatcher(): CoroutineDispatcher =
    if (useCoroutinesScheduler) DefaultScheduler else CommonPool
internal object DefaultScheduler : ExperimentalCoroutineDispatcher() {
    .....
}
//委托类
public open class ExperimentalCoroutineDispatcher(
    private val corePoolSize: Int,
    private val maxPoolSize: Int,
    private val idleWorkerKeepAliveNs: Long,
    private val schedulerName: String = "CoroutineScheduler"
) : ExecutorCoroutineDispatcher() {
}
//java类库中的Executor实现线程池逻辑
internal object CommonPool : ExecutorCoroutineDispatcher() {}
//共同父类,定义行为
public abstract class ExecutorCoroutineDispatcher: CoroutineDispatcher(), Closeable {}
复制代码
ExperimentalCoroutineDispatcher
  • DefaultScheduler的主要实现都在它的父类ExperimentalCoroutineDispatcher中
public open class ExperimentalCoroutineDispatcher(
    private val corePoolSize: Int,
    private val maxPoolSize: Int,
    private val idleWorkerKeepAliveNs: Long,
    private val schedulerName: String = "CoroutineScheduler"
) : ExecutorCoroutineDispatcher() {
    public constructor(
        corePoolSize: Int = CORE_POOL_SIZE,
        maxPoolSize: Int = MAX_POOL_SIZE,
        schedulerName: String = DEFAULT_SCHEDULER_NAME
    ) : this(corePoolSize, maxPoolSize, IDLE_WORKER_KEEP_ALIVE_NS, schedulerName)

    ...//省略构造
    
    //创建CoroutineScheduler实例
    private fun createScheduler() = CoroutineScheduler(corePoolSize, maxPoolSize, idleWorkerKeepAliveNs, schedulerName)
    
    override val executor: Executorget() = coroutineScheduler

    override fun dispatch(context: CoroutineContext, block: Runnable): Unit =
        try {
            //dispatch方法委托到CoroutineScheduler的dispatch方法
            coroutineScheduler.dispatch(block)
        } catch (e: RejectedExecutionException) {
            ....
        }

    override fun dispatchYield(context: CoroutineContext, block: Runnable): Unit =
        try {
            //dispatchYield方法委托到CoroutineScheduler的dispatchYield方法
            coroutineScheduler.dispatch(block, tailDispatch = true)
        } catch (e: RejectedExecutionException) {
            ...
        }
    
	internal fun dispatchWithContext(block: Runnable, context: TaskContext, tailDispatch: Boolean) {
        try {
            //dispatchWithContext方法委托到CoroutineScheduler的dispatchWithContext方法
            coroutineScheduler.dispatch(block, context, tailDispatch)
        } catch (e: RejectedExecutionException) {
            ....
        }
    }
    override fun close(): Unit = coroutineScheduler.close()
    //实现请求阻塞
    public fun blocking(parallelism: Int = BLOCKING_DEFAULT_PARALLELISM): CoroutineDispatcher {
        require(parallelism > 0) { "Expected positive parallelism level, but have $parallelism" }
        return LimitingDispatcher(this, parallelism, null, TASK_PROBABLY_BLOCKING)
    }
		//实现请求数量限制
    public fun limited(parallelism: Int): CoroutineDispatcher {
        require(parallelism > 0) { "Expected positive parallelism level, but have $parallelism" }
        require(parallelism <= corePoolSize) { "Expected parallelism level lesser than core pool size ($corePoolSize), but have $parallelism" }
        return LimitingDispatcher(this, parallelism, null, TASK_NON_BLOCKING)
    }
    
    ....//省略一些供测试的方法,更好的跟踪同步状态
}
复制代码
3.1.1.2. IO
  • IO的实现其实是LimitingDispatcher
val IO: CoroutineDispatcher = LimitingDispatcher(
    this,
    systemProp(IO_PARALLELISM_PROPERTY_NAME, 64.coerceAtLeast(AVAILABLE_PROCESSORS)),
    "Dispatchers.IO",
    TASK_PROBABLY_BLOCKING
)
复制代码
LimitingDispatcher
  • IO的实现类会有一些最大请求限制,以及队列处理
private class LimitingDispatcher(
    private val dispatcher: ExperimentalCoroutineDispatcher,
    private val parallelism: Int,
    private val name: String?,
    override val taskMode: Int
) : ExecutorCoroutineDispatcher(), TaskContext, Executor {
    //同步阻塞队列
    private val queue = ConcurrentLinkedQueue<Runnable>()
    //cas计数
    private val inFlightTasks = atomic(0)
    
    override fun dispatch(context: CoroutineContext, block: Runnable) = dispatch(block, false)

    private fun dispatch(block: Runnable, tailDispatch: Boolean) {
        var taskToSchedule = block
        while (true) {

            if (inFlight <= parallelism) {
                //LimitingDispatcher的dispatch方法委托给了DefaultScheduler的dispatchWithContext方法
                dispatcher.dispatchWithContext(taskToSchedule, this, tailDispatch)
                return
            }
            ..//省略了一些队列处理逻辑
        }
    }
}
复制代码

3.1.2. CoroutineScheduler

  • Default、IO其实都是共享CoroutineScheduler线程池,Kotlin实现了一套线程池两种调度策略
  • 通过内部的mode区分
fun dispatch(block: Runnable, taskContext: TaskContext = NonBlockingContext, tailDispatch: Boolean = false) {
......
    if (task.mode == TASK_NON_BLOCKING) {
        if (skipUnpark) return
        signalCpuWork()
    } else {
        signalBlockingWork(skipUnpark = skipUnpark)
    }
}
复制代码
Mode
TypeMode
DefaultTASK_NON_BLOCKING
IOTASK_PROBABLY_BLOCKING
处理策略
TypeMode
DefaultCoroutineScheduler最多有corePoolSize个线程被创建,corePoolSize它的取值为max(2, CPU核心数)
即它会尽量的等于CPU核心数
IO创建比corePoolSize更多的线程来运行IO型任务,但不能大于maxPoolSize
1.公式:max(corePoolSize, min(CPU核心数 * 128, 2^21 - 2)),即大于corePoolSize,小于2^21 - 2
2.2^21 - 2是一个很大的数约为2M,但是CoroutineScheduler是不可能创建这么多线程的,所以就需要外部限制提交的任务数
3.Dispatchers.IO构造时就通过LimitingDispatcher默认限制了最大线程并发数parallelism为max(64, CPU核心数),即最多只能提交parallelism个任务到CoroutineScheduler中执行,剩余的任务被放进队列中等待。
适合场景
TypeMode
Default1.CPU密集型任务的特点是执行任务时CPU会处于忙碌状态,任务会消耗大量的CPU资源
2.复杂计算、视频解码等,如果此时线程数太多,超过了CPU核心数,那么这些超出来的线程是得不到CPU的执行的,只会浪费内存资源
3.因为线程本身也有栈等空间,同时线程过多,频繁的线程切换带来的消耗也会影响线程池的性能
4.对于CPU密集型任务,线程池并发线程数等于CPU核心数才能让CPU的执行效率最大化
IO1.IO密集型任务的特点是执行任务时CPU会处于闲置状态,任务不会消耗大量的CPU资源
2.网络请求、IO操作等,线程执行IO密集型任务时大多数处于阻塞状态,处于阻塞状态的线程是不占用CPU的执行时间
3.此时CPU就处于闲置状态,为了让CPU忙起来,执行IO密集型任务时理应让线程的创建数量更多一点,理想情况下线程数应该等于提交的任务数,对于这些多创建出来的线程,当它们闲置时,线程池一般会有一个超时回收策略,所以大部分情况下并不会占用大量的内存资源
4.但也会有极端情况,所以对于IO密集型任务,线程池并发线程数应尽可能地多才能提高CPU的吞吐量,这个尽可能地多的程度并不是无限大,而是根据业务情况设定,但肯定要大于CPU核心数。

3.1.3. Unconfined

  • 任务执行在默认的启动线程。之后由调用resume的线程决定恢复协程的线程。
internal object Unconfined : CoroutineDispatcher() {
    //为false为不需要dispatch
    override fun isDispatchNeeded(context: CoroutineContext): Boolean = false

    override fun dispatch(context: CoroutineContext, block: Runnable) {
        // 只有当调用yield方法时,Unconfined的dispatch方法才会被调用
        // yield() 表示当前协程让出自己所在的线程给其他协程运行
        val yieldContext = context[YieldContext]
        if (yieldContext != null) {
            yieldContext.dispatcherWasUnconfined = true
            return
        }
        throw UnsupportedOperationException("Dispatchers.Unconfined.dispatch function can only be used by the yield function. " +
            "If you wrap Unconfined dispatcher in your code, make sure you properly delegate " +
            "isDispatchNeeded and dispatch calls.")
    }
}
复制代码
  • 每一个协程都有对应的Continuation实例,其中的resumeWith用于协程的恢复,存在于DispatchedContinuation
DispatchedContinuation
  • 我们重点看resumeWith的实现以及类委托
internal class DispatchedContinuation<in T>(
    @JvmField val dispatcher: CoroutineDispatcher,
    @JvmField val continuation: Continuation<T>
) : DispatchedTask<T>(MODE_UNINITIALIZED), CoroutineStackFrame, Continuation<T> by continuation {
    .....
    override fun resumeWith(result: Result<T>) {
        val context = continuation.context
        val state = result.toState()
        if (dispatcher.isDispatchNeeded(context)) {
            _state = state
            resumeMode = MODE_ATOMIC
            dispatcher.dispatch(context, this)
        } else {
            executeUnconfined(state, MODE_ATOMIC) {
                withCoroutineContext(this.context, countOrElement) {
                    continuation.resumeWith(result)
                }
            }
        }
    }
    ....
}
复制代码

解析如下:

  1. DispatchedContinuation通过类委托实现了在resumeWith()方法之前的代码逻辑添加

  2. 通过isDispatchNeeded(是否需要dispatch,Unconfined=false,default,IO=true)判断做不同处理

    1. true:调用协程的CoroutineDispatcher的dispatch方法
    2. false:调用executeUnconfined方法
    private inline fun DispatchedContinuation<*>.executeUnconfined(
        contState: Any?, mode: Int, doYield: Boolean = false,
        block: () -> Unit
    ): Boolean {
        assert { mode != MODE_UNINITIALIZED }
        val eventLoop = ThreadLocalEventLoop.eventLoop
        if (doYield && eventLoop.isUnconfinedQueueEmpty) return false
        return if (eventLoop.isUnconfinedLoopActive) {
            _state = contState
            resumeMode = mode
            eventLoop.dispatchUnconfined(this)
            true
        } else {
            runUnconfinedEventLoop(eventLoop, block = block)
            false
        }
    }
    复制代码
    1. 从threadlocal中取出eventLoop(eventLoop和当前线程相关的),判断是否在执行Unconfined任务
      1. 如果在执行则调用EventLoop的dispatchUnconfined方法把Unconfined任务放进EventLoop中
      2. 如果没有在执行则直接执行
    internal inline fun DispatchedTask<*>.runUnconfinedEventLoop(
        eventLoop: EventLoop,
        block: () -> Unit
    ) {
        eventLoop.incrementUseCount(unconfined = true)
        try {
            block()
            while (true) {
                if (!eventLoop.processUnconfinedEvent()) break
            }
        } catch (e: Throwable) {
            handleFatalException(e, null)
        } finally {
            eventLoop.decrementUseCount(unconfined = true)
        }
    }
    复制代码
    1. 执行block()代码块,即上文提到的resumeWith()
    2. 调用processUnconfinedEvent()方法实现执行剩余的Unconfined任务,知道全部执行完毕跳出循环
EventLoop
  • EventLoop是存放与threadlocal,所以是跟当前线程相关联的,而EventLoop也是CoroutineDispatcher的一个子类
internal abstract class EventLoop : CoroutineDispatcher() {
  	.....
    //双端队列实现存放Unconfined任务
    private var unconfinedQueue: ArrayQueue<DispatchedTask<*>>? = null
    //从队列的头部移出Unconfined任务执行
    public fun processUnconfinedEvent(): Boolean {
        val queue = unconfinedQueue ?: return false
        val task = queue.removeFirstOrNull() ?: return false
        task.run()
        return true
    }
    //把Unconfined任务放进队列的尾部
    public fun dispatchUnconfined(task: DispatchedTask<*>) {
        val queue = unconfinedQueue ?:
            ArrayQueue<DispatchedTask<*>>().also { unconfinedQueue = it }
        queue.addLast(task)
    }
    .....
}
复制代码

解析如下:

  1. 内部通过双端队列实现存放Unconfined任务
    1. EventLoop的dispatchUnconfined方法用于把Unconfined任务放进队列的尾部
    2. rocessUnconfinedEvent方法用于从队列的头部移出Unconfined任务执行

3.1.4. Main

  • 是把协程运行在平台相关的只能操作UI对象的Main线程,但是根据不同平台有不同的实现
平台实现
kotlin/jskotlin对JavaScript的支持,提供了转换kotlin代码,kotlin标准库的能力,npm包管理能力
在kotlin/js上Dispatchers.Main等效于Dispatchers.Default
kotlin/native将kotlin代码编译为无需虚拟机就可运行的原生二进制文件的技术, 它的主要目的是允许对不需要或不可能使用虚拟机的平台进行编译,例如嵌入式设备或iOS
在kotlin/native上Dispatchers.Main等效于Dispatchers.Default
kotlin/JVM需要虚拟机才能编译的平台,例如Android就是属于kotlin/JVM,对于kotlin/JVM我们需要引入对应的dispatcher,例如Android就需要引入kotlinx-coroutines-android库,它里面有Android对应的Dispatchers.Main实现,其实就是把任务通过Handler运行在Android的主线程

3.2. CoroutineName Element

  • 协程名称,可以自定义,方便调试分析
public data class CoroutineName(
    val name: String
) : AbstractCoroutineContextElement(CoroutineName) {
 
    public companion object Key : CoroutineContext.Key<CoroutineName>
    
    override fun toString(): String = "CoroutineName($name)"
}
复制代码

3.3. CoroutineExceptionHandler Element

  • 协程异常处理器,默认创建的协程都会有一个异常处理器,也可以手动指定。

    var coroutineContext = Job() +
            Dispatchers.Main +
    	   //手动添加指定异常处理器
            CoroutineExceptionHandler { coroutineContext, throwable ->
                Log.e(
                    "CoroutineException",
                    "CoroutineExceptionHandler: $throwable"
                )
            } +
            CoroutineName("asyncConcurrent")
    复制代码
  • 但是只对launch方法启动的根协程有效,而对async启动的根协程无效

    async启动的根协程默认会捕获所有未捕获异常并把它放在Deferred中,等到用户调用Deferred的await方法才抛出,也就是需要手动加try-catch

CASE

协程的使用场景变化自如,异常处理的情况也就比较多

  1. 非SupervisorJob情况下,字协程抛出的异常会委托给父协程的CoroutineExceptionHandler处理
    1. 子协程的CoroutineExceptionHandler并不会执行
  2. SupervisorJob情况下,不会产生异常传播,即自己的CoroutineExceptionHandler可以接收到异常
  3. 子协程同时抛出多个异常时,CoroutineExceptionHandler只会捕捉第一个异常,后续的异常存于第一个异常的suppressed数组之中
  4. 取消协程时会抛出CancellationException,但是所有的CoroutineExceptionHandler不会接收到,只能通过try-catch实现捕获

3.4. CoroutineContext结构

CoroutineContext.png

  • CoroutineContext是一个特殊的集合,同时包含了Map和Set的特点
  • 集合内部的元素Element是根据key去对应(Map特点),但是不允许重复(Set特点)
  • Element之间可以通过+号进行组合
  • 每一个Element都继承与CoroutineContext
public interface CoroutineContext {
    //操作符[]重载,可以通过CoroutineContext[Key]来获取与Key关联的Element
    public operator fun <E : Element> get(key: Key<E>): E?

    //它是一个聚集函数,提供了从left到right遍历CoroutineContext中每一个Element的能力,并对每一个Element做operation操作
    public fun <R> fold(initial: R, operation: (R, Element) -> R): R
    
 	 //操作符+重载,可以CoroutineContext + CoroutineContext这种形式把两个CoroutineContext合并成一个
    public operator fun plus(context: CoroutineContext): CoroutineContext =
        if (context === EmptyCoroutineContext) this else 
            context.fold(this) { acc, element ->
                val removed = acc.minusKey(element.key)
                if (removed === EmptyCoroutineContext) element else {
                    val interceptor = removed[ContinuationInterceptor]
                    if (interceptor == null) CombinedContext(removed, element) else {
                        val left = removed.minusKey(ContinuationInterceptor)
                        if (left === EmptyCoroutineContext) CombinedContext(element, interceptor) else
                            CombinedContext(CombinedContext(left, element), interceptor)
                    }
                }
            }
    
    //返回一个新的CoroutineContext,这个CoroutineContext删除了Key对应的Element
    public fun minusKey(key: Key<*>): CoroutineContext

    //Key定义,空实现
    public interface Key<E : Element>
    
    //Element定义,每个Element都是一个CoroutineContext
    public interface Element : CoroutineContext {
        .....
    }
}
复制代码

这里使用了Kotlin的operator操作符重载实现了各种策略

  • 除了plus方法,CoroutineContext中的其他三个方法都被CombinedContext、Element、EmptyCoroutineContext重写
    • CombinedContext:CoroutineContext集合结构的实现,它里面是一个递归定义
    • Element:CombinedContext中的元素
    • EmptyCoroutineContext:一个空的CoroutineContext,它里面是空实现

CombinedContext

internal class CombinedContext(
    private val left: CoroutineContext,
    private val element: Element
) : CoroutineContext, Serializable {
    ....
}
复制代码
  • CombinedContext包含left和element元素
    • left可能是element或者是CombinedContext
    • element就是element
get方法
  • 可以通过CoroutineContext[Key]来获取与Key关联的Element
override fun <E : Element> get(key: Key<E>): E? {
    var cur = this
    while (true) {
        //element是否匹配,如果是则直接返回,即匹配成功
        cur.element[key]?.let { return it }
        //没有匹配成功则从left开始寻找
        val next = cur.left
        //如果left是CombinedContext,则改变next,重复上述步骤
        if (next is CombinedContext) {
            cur = next
        } else {
            //匹配成功则返回
            return next[key]
        }
    }
}
复制代码
fold方法
  • 提供了从left到right遍历CoroutineContext中每一个Element的能力,并对每一个Element做operation操作
//operation是一个函数指针,可以执行函数引用
public override fun <R> fold(initial: R, operation: (R, Element) -> R): R =
    //对left做fold操作,把left做完fold操作的的返回结果和element做operation操作
    operation(left.fold(initial, operation), element)
复制代码
minusKey方法
  • 返回一个新的CoroutineContext,这个CoroutineContext删除了Key对应的Element
public override fun minusKey(key: Key<*>): CoroutineContext {
        //element是否匹配,如果是则直接返回供删除,即匹配成功
        element[key]?.let { return left }
        //没有匹配成功则从left开始寻找
        val newLeft = left.minusKey(key)
        return when {
            //如果left中不存在目标element,则当前CombinedContext肯定不包含目标元素,直接返回当前
            newLeft === left -> this
            //如果left之中存在目标element,删除目标element后,left等于空,返回当前CombinedContext的element
            newLeft === EmptyCoroutineContext -> element
            //如果left之中存在目标element,删除目标element后,left不等于空,创建新的CombinedContext并返回
            else -> CombinedContext(newLeft, element)
        }
}
复制代码
结构图

CoroutineContext结构.png

  • 整体像链表,left就是指向下一个结点的指针,
  • get、minusKey操作逻辑流程都是先访问当前element,不满足,再访问left的element,顺序都是从right到left
  • fold的操作逻辑流程是先访问left,直到递归到最后的element,然后再从left到right的返回,从而访问了所有的element。

plus方法

此方法是CoroutineContext的实现,内部分为元素合并和拦截器处理

public operator fun plus(context: CoroutineContext): CoroutineContext =
	    //如果合并的元素为空,直接返回
        if (context === EmptyCoroutineContext) this else 
		   //对要合并的元素flod处理
            context.fold(this) { acc, element -> //acc==原有元素,element==合并元素
                //从原有元素之中尝试删除合并元素
                val removed = acc.minusKey(element.key)
                //如果删除元素为空则说明原有元素删除了和目标元素的相同元素后为空,则直接返回目标元素即可
                if (removed === EmptyCoroutineContext) element else {
                    //尝试获取对应的Interceptor
                    val interceptor = removed[ContinuationInterceptor]
                    //如果Interceptor为空则构建一个新的元素
                    if (interceptor == null) CombinedContext(removed, element) else {
                        //如果不为空则做删除操作
                        val left = removed.minusKey(ContinuationInterceptor)
                        if (left === EmptyCoroutineContext) CombinedContext(element, interceptor) else
                        	//如果删除元素为空则说明原有元素删除了和目标元素的相同元素后不为空,则构建一个新的元素并返回,并添加Interceptor至末尾
                            CombinedContext(CombinedContext(left, element), interceptor)
                    }
                }
            }
复制代码
  • puls方法最终返回的CoroutineContext是不存在key相同的element的,新增元素会覆盖CoroutineContext中的含有相同key的元素,这像是Set的特性

  • 拦截器的处理就是为了每次添加完成后保持ContinuationInterceptor为CoroutineContext中的最后一个元素,目的是在执行协程之前做前置操作

    CoroutineDispatcher就继承自ContinuationInterceptor

    • 通过把ContinuationInterceptor放在最后面,协程在查找上下文的element时,总能最快找到拦截器,避免了递归查找,从而让拦截行为前置执行

4. Job Element

  • 每一个所创建的协程 (通过 launch 或者 async),会返回一个 Job实例,该实例是协程的唯一标识,并且负责管理协程的生命周期

4.1. Job状态

Job在执行的过程中,包含了一系列状态,虽然开发者没办法直接获取所有状态,但是Job之中有如下三个属性

  • isActive(是否活动)
  • isCompleted(是否已完成)
  • isCancelled(是否已取消)

根据属性就可以推断出Job的所处状态,状态如下

  • 新创建 (New)
    • 当一个协程创建后就处于新建(New)状态
  • 活跃 (Active)
    • 当调用Job的start/join方法后协程就处于活跃(Active)状态
  • 完成中 (Completing)
    • 当协程执行完成后或者调用CompletableJob(CompletableJob是Job的一个子接口)的complete方法都会让当前协程进入完成中(Completing)状态
  • 已完成 (Completed)
    • 处于完成中状态的协程会等所有子协程都完成后才进入完成(Completed)状态
  • 取消中 (Cancelling)
    • 当运行出错或者调用Job的cancel方法都会将当前协程置为取消中(Cancelling)状态
  • 已取消 (Cancelled)
    • 处于取消中状态的协程会等所有子协程都完成后才进入取消 (Cancelled)状态
StateisActiveisCompletedisCancelled
New (optional initial state)falsefalsefalse
Active (default initial state)truefalsefalse
Completing (transient state)truefalsefalse
Cancelling (transient state)falsefalsetrue
Cancelled (final state)falsetruetrue
Completed (final state)falsetruefalse
                                      wait children
+-----+ start  +--------+ complete   +-------------+  finish  +-----------+
| New | -----> | Active | ---------> | Completing  | -------> | Completed |
+-----+        +--------+            +-------------+          +-----------+
                 |  cancel / fail       |
                 |     +----------------+
                 |     |
                 V     V
             +------------+                           finish  +-----------+
             | Cancelling | --------------------------------> | Cancelled |
             +------------+                                   +-----------+

复制代码

4.2. Job方法

fun start(): Boolean
  • 调用该函数来启动这个 Coroutine
  • 如果当前 Coroutine 还没有执行调用该函数返回 true
  • 如果当前 Coroutine 已经执行或者已经执行完毕,则调用该函数返回 false
fun cancel(cause: CancellationException? = null)
  • 通过可选的取消原因取消Job
fun invokeOnCompletion(handler: CompletionHandler): DisposableHandle
  • 通过这个函数可以给 Job 设置一个完成通知,当 Job 执行完成的时候会同步执行这个通知函数。 回调的通知对象类型为:typealias CompletionHandler = (cause: Throwable?) -> Unit.
  • CompletionHandler 参数代表了 Job 是如何执行完成的。 cause 有下面三种情况:
    • 如果 Job 是正常执行完成的,则 cause 参数为 null
    • 如果 Job 是正常取消的,则 cause 参数为 CancellationException 对象。这种情况不应该当做错误处理,这是任务正常取消的情形。所以一般不需要在错误日志中记录这种情况。
    • 其他情况表示 Job 执行失败了。
  • 这个函数的返回值为 DisposableHandle 对象,如果不再需要监控 Job 的完成情况了, 则可以调用 DisposableHandle.dispose 函数来取消监听。如果 Job 已经执行完了, 则无需调用 dispose 函数了,会自动取消监听。
suspend fun join()(suspend函数)
  • 用来在另外一个 Coroutine 中等待 job 执行完成后继续执行。

4.3. Job异常传播

  • 协程是有父子级的概念,如果子Job在运行过程之中发生异常,那么父Job就会感知到并抛出异常。如果要抑制这种行为就需要使用SupervisorJob

    除了CancellationException以外的异常

SupervisorJob
fun main(){
     val parentJob = GlobalScope.launch {
       //childJob是一个SupervisorJob
        val childJob = launch(SupervisorJob()){
            throw NullPointerException()
        }
        childJob.join()
        println("parent complete")
    }
    Thread.sleep(1000)
}
复制代码

此时childJob抛出异常并不会影响parentJob的运行,parentJob会继续运行并输出parent complete。


5. CoroutineScope

  • CoroutineScope是用于提供CoroutineContext的容器,但是制定了代码边界,去全局管控所有内部作用域中的CoroutineContext,源码如下:
public interface CoroutineScope {
    public val coroutineContext: CoroutineContext
}
复制代码

这里只需要一个CoroutineContext,保证CoroutineContext能在整个协程运行中传递下去,约束CoroutineContext的作用边界

5.1. lifecycleScope

  • lifecycleScope可以让协程具有与Activity一样的生命周期意识
public val LifecycleOwner.lifecycleScope: LifecycleCoroutineScope
    get() = lifecycle.coroutineScope
    
//初始化以及控制异常传播
public val Lifecycle.coroutineScope: LifecycleCoroutineScope
    get() {
        while (true) {
            val existing = mInternalScopeRef.get() as LifecycleCoroutineScopeImpl?
            if (existing != null) {
                return existing
            }
            val newScope = LifecycleCoroutineScopeImpl(
                this,
                SupervisorJob() + Dispatchers.Main.immediate
            )
            if (mInternalScopeRef.compareAndSet(null, newScope)) {
                newScope.register()
                return newScope
            }
        }
    }

//实现生命周期感知并调用关键方法
internal class LifecycleCoroutineScopeImpl(
    override val lifecycle: Lifecycle,
    override val coroutineContext: CoroutineContext
) : LifecycleCoroutineScope(), LifecycleEventObserver {
    init {
        if (lifecycle.currentState == Lifecycle.State.DESTROYED) {
            coroutineContext.cancel()
        }
    }
 
    fun register() {
        launch(Dispatchers.Main) {
            if (lifecycle.currentState >= Lifecycle.State.INITIALIZED) {
                lifecycle.addObserver(this@LifecycleCoroutineScopeImpl)
            } else {
                coroutineContext.cancel()
            }
        }
    }

    override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
        if (lifecycle.currentState <= Lifecycle.State.DESTROYED) {
            lifecycle.removeObserver(this)
            coroutineContext.cancel()
        }
    }
  
//扩展取消实现
public fun CoroutineContext.cancel(): Unit {
    this[Job]?.cancel()
}
复制代码
  • 源码解析如下:
    1. 通过创建LifecycleCoroutineScopeImpl实现CoroutineScope接口
    2. 通过SupervisorJob控制异常传播
    3. 通过Dispatchers.Main控制线程类型
    4. 在register方法中通过launch创建协程,通过lifecycle的状态监听Activity的生命周期,在合适的时机调用cancel方法
    5. 通过扩展实现取消策略

5.2. 其他方法

  • lifecycleScope还扩展出了其他作用域范围的控制函数
lifecycleScope.launchWhenCreated {  }
lifecycleScope.launchWhenStarted {  }
lifecycleScope.launchWhenResumed {  }
复制代码

6. ContinuationInterceptor

  • ContinuationInterceptor继承于CoroutineContext.Element,也就是CoroutineContext
  • ContinuationInterceptor提供了interceptContinuation方法,实现了拦截,源码分析如下:

6.1. intercepted调用链

public interface ContinuationInterceptor : CoroutineContext.Element {
    companion object Key : CoroutineContext.Key<ContinuationInterceptor>
    public fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T>
    ...
}
//1. 通过launch调用链分析流程
public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job {
    val newContext = newCoroutineContext(context)
    val coroutine = if (start.isLazy)
        LazyStandaloneCoroutine(newContext, block) else
  			//2. 通过StandaloneCoroutine创建
        StandaloneCoroutine(newContext, active = true)
  	//3. 启动
    coroutine.start(start, coroutine, block)
    return coroutine
}

//开启方法
public fun <R> start(start: CoroutineStart, receiver: R, block: suspend R.() -> T) {
    initParentJob()
    start(block, receiver, this)
}

public operator fun <R, T> invoke(block: suspend R.() -> T, receiver: R, completion: Continuation<T>) =
    when (this) {
        // 开启方法中的CoroutineStart 默认为 startCoroutineCancellable
        CoroutineStart.DEFAULT -> block.startCoroutineCancellable(receiver, completion)
        CoroutineStart.ATOMIC -> block.startCoroutine(receiver, completion)
        CoroutineStart.UNDISPATCHED -> block.startCoroutineUndispatched(receiver, completion)
        CoroutineStart.LAZY -> Unit // will start lazily
    }

internal fun <R, T> (suspend (R) -> T).startCoroutineCancellable(receiver: R, completion: Continuation<T>) =
    runSafely(completion) {
        //创建协程,并调用intercepted方法拦截,最终调用resumeCancellable,其本质就是resumeWith
        //最终每次启动协程都会自动回调一次resumeWith方法
        createCoroutineUnintercepted(receiver, completion).intercepted().resumeCancellable(Unit)
    }
复制代码

6.2. intercepted具体实现

//声明的跨平台实现接口
public expect fun <T> Continuation<T>.intercepted(): Continuation<T>
//android平台具体实现
public actual fun <T> Continuation<T>.intercepted(): Continuation<T> =
    (this as? ContinuationImpl)?.intercepted() ?: this
    
//最终的调用方法
public fun intercepted(): Continuation<Any?> =
    intercepted
				//首次intercepted才会调用interceptContinuation方法,也就是拦截处理过的Continuation
        ?: (context[ContinuationInterceptor]?.interceptContinuation(this) ?: this)
            .also { intercepted = it }
复制代码

6.3. 实例代码

  • ContinuationInterceptor可以实现在协程启动的时进行拦截,下面是一个简单实例
GlobalScope.launch(object : ContinuationInterceptor {
    override val key: CoroutineContext.Key<*> = ContinuationInterceptor

    override fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T> {
        println("intercept")
        return object : Continuation<T> by continuation {
            override fun resumeWith(result: Result<T>) {
                println("create new thread")
                thread {
                    continuation.resumeWith(result)
                }
            }
        }
    }
}) {
    withContext(Dispatchers.IO) {
      //do something
    }
}.start()
复制代码

根据实例引出几个上文分析的问题:

6.4. 为什么在plus方法之中需要保持ContinuationInterceptor为CoroutineContext中的最后一个元素?

  1. 为了提高查找ContinuationInterceptor的速度,才会添加至末端。原因如下:
    • CombinedContext的结构是leftelement,而left类似于前驱节,类似前驱集合,
    • element只是一个纯碎的CoroutineContext,而它的get方法每次都是从element开始进行查找对应KeyCoroutineContext对象
    • 最终没有匹配到才会去left集合中进行递归查找
  2. ContinuationInterceptor使用的很频繁,其实每次创建都要查询当前协程的CoroutineContext中是否存在`ContinuationInterceptor

7. Suspend|协程状态机

  • 被suspend关键字修饰的方法为协程方法,其本质如下:

7.1. CPS机制

  • 通过CPS(Continuation-Passing-Style)机制。使得每一个suspend修饰的方法或者lambda表达式都会在代码调用的时候为其额外添加Continuation类型的参数
//转换前
@GET("users/{user}/repos")
suspend fun getRepoListByUser(@Path("user") user: String): ApiResponse<List<Repo>>
//转换后
@GET("/v2/news")
fun getRepoListByUser(@Path("user") user: String, c: Continuation<List<Repo>>): Any?
复制代码
  • 之所以返回变为Any是因为:
    • suspend函数被协程挂起时,它会返回一个特殊的标识COROUTINE_SUSPENDED,而它本质就是一个Any
    • 当协程不挂起进行执行时,它将返回执行的结果或者引发的异常。
    • 所以使用了Kotlin独有的Any?类型。
  • Continuation源码如下:
public interface Continuation<in T> {
    public val context: CoroutineContext
    public fun resumeWith(result: Result<T>)
}
复制代码
  • 属性解析:
    • context:协程的上下文,多数情况是CombinedContext类型
    • resumeWith:唤醒挂起的协程,当协程内部的逻辑执行完毕之后,就是通过该方法来唤起协程,让它在之前挂起的位置继续执行下去。
  • Continuation获取时机:
    • Continutation与协程创建的时候一起被创建的,launch的时候就已经创建了Continutation对象,并且启动了协程。
    • launch代码块里进行的挂起函数传递的参数都是这个对象。

7.2. 协程状态机

协程通过suspend来标识挂起点,但真正的挂起点还需要通过是否返回COROUTINE_SUSPENDED来判断,而代码体现是通过状态机来处理协程的挂起与恢复。在需要挂起的时候,先保留现场与设置下一个状态点,然后再通过退出方法的方式来挂起协程。在挂起的过程中并不会阻塞当前的线程。对应的恢复通过resumeWith来进入状态机的下一个状态,同时在进入下一个状态时会恢复之前挂起的现场

  • 我们结合kotlin字节码文件分析协程状态机
public final class TextActivity extends ComponentActivity {
   protected void onCreate(@Nullable Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
         //......
         public final Object invokeSuspend(@NotNull Object $result) {
            //挂起标识 
            Object var2 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
            //通过label实现不同状态的处理,内部的case是逻辑代码
            switch(this.label) {
            case 0:
               ResultKt.throwOnFailure($result);
               TextActivity var10000 = TextActivity.this;
               this.label = 1;
               if (var10000.test(this) == var2) {
                  return var2;
               }
               break;
            case 1:
               ResultKt.throwOnFailure($result);
               break;
            case 2:
              // 执行对应逻辑 2秒之后被唤醒
               ResultKt.throwOnFailure($result);
               $this$async = this.p$;
               this.L$0 = $this$async;
               this.label = 1;
                 if (DelayKt.delay(2000L, this) == var3) {
                     return var3;
                  }
               break;
            default:
               throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
            }

            return Unit.INSTANCE;
         }
     	   //......
         public final Object invoke(Object var1, Object var2) {
            // 协程启动的时候会调用一次resumeWith方法,而它对应的内部逻辑就是invokeSuspend方法
            return ((<undefinedtype>)this.create(var1, (Continuation)var2)).invokeSuspend(Unit.INSTANCE);
         }
      }), 3, (Object)null);
   }
   //......
}
复制代码
  1. getCOROUTINE_SUSPENDED方法也就是上文说的COROUTINE_SUSPENDED标识用于标识挂起
  2. 通过label实现不同状态的处理,在对应的case返回挂起标识
  3. 当返回了COROUTINE_SUSPENDED也就会跳出方法,此时协程就被挂起。当前线程也就可以执行其它的逻辑,并不会被协程的挂起所阻塞
  4. 最终等到下个状态,执行对应的代码
  5. 在label进入case2状态,会在对应时间字之后实现唤醒

分类:
Android
标签:
收藏成功!
已添加到「」, 点击更改