协程线程切换原理

311 阅读3分钟

在前面讲如何开启一个原始协程的时候,我们知道:

协程执行流程有3步,createCoroutineUnintercepted创建、intercepted拦截、resumeWith执行

看到intercepted,自然而然想到的就是拦截

查看源码:

public fun intercepted(): Continuation<Any?> =
        intercepted
            ?: (context[ContinuationInterceptor]?.interceptContinuation(this) ?: this)
                .also { intercepted = it }
public interface ContinuationInterceptor : CoroutineContext.Element {
    companion object Key : CoroutineContext.Key<ContinuationInterceptor>

    public fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T>
}

ContinuationInterceptor继承于CoroutineContext.Element,所以它也是CoroutineContext,同时提供了interceptContinuation方法,先记住这个方法后续会用到。

现在我们来看Dispatchers.Main,为什么它能切换到主线程呢?

public actual val Main: MainCoroutineDispatcher get() = MainDispatcherLoader.dispatcher
public abstract class MainCoroutineDispatcher : CoroutineDispatcher()

发现MainCoroutineDispatcher继承于CoroutineDispatcher,主角登场了,但还不够,我们继续看CoroutineDispatcher是什么

public abstract class CoroutineDispatcher :
    AbstractCoroutineContextElement(ContinuationInterceptor), ContinuationInterceptor {
    
    public open fun isDispatchNeeded(context: CoroutineContext): Boolean = true
    
    public abstract fun dispatch(context: CoroutineContext, block: Runnable)
    
    public open fun dispatchYield(context: CoroutineContext, block: Runnable) = dispatch(context, block)
    
    public final override fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T> =
        DispatchedContinuation(this, continuation)
}

原来CoroutineDispatcher实现了ContinuationInterceptor,说明CoroutineDispatcher也具有拦截器的功能

再来看CoroutineDispatcher提供的几个方法:

  • isDispatchNeeded:判断是否需要分发
  • dispatch:如何进行分发
  • interceptContinuation:直接返回了DispatchedContinuation对象,它是一个Continuation类型。那么自然重点就是它的resumeWith方法。
override fun resumeWith(result: Result<T>) {
    val context = continuation.context
    val state = result.toState()
    if (dispatcher.isDispatchNeeded(context)) {
        _state = state
        resumeMode = MODE_ATOMIC_DEFAULT
        dispatcher.dispatch(context, this)
    } else {
        executeUnconfined(state, MODE_ATOMIC_DEFAULT) {
            withCoroutineContext(this.context, countOrElement) {
                continuation.resumeWith(result)
            }
        }
    }
}

这里我们看到了isDispatchNeededdispatch方法,如果不需要分发自然是直接调用原始的continuation对象的resumeWith方法,也就没有什么类似于线程的切换。

那什么时候isDispatcheNeededtrue呢?这就要看它的dispatcer是什么。

由于现在我们是拿Dispatchers.Main作分析。所以这里我直接告诉你们它的dispatcherHandlerContext

override fun createDispatcher(allFactories: List<MainDispatcherFactory>) =
    HandlerContext(Looper.getMainLooper().asHandler(async = true), "Main")

internal class HandlerContext private constructor(
    private val handler: Handler,
    private val name: String?,
    private val invokeImmediately: Boolean
) : HandlerDispatcher(), Delay {
    /**
     * Creates [CoroutineDispatcher] for the given Android [handler].
     *
     * @param handler a handler.
     * @param name an optional name for debugging.
     */
    public constructor(
        handler: Handler,
        name: String? = null
    ) : this(handler, name, false)

    @Volatile
    private var _immediate: HandlerContext? = if (invokeImmediately) this else null

    override val immediate: HandlerContext = _immediate ?:
        HandlerContext(handler, name, true).also { _immediate = it }

    override fun isDispatchNeeded(context: CoroutineContext): Boolean {
        return !invokeImmediately || Looper.myLooper() != handler.looper
    }

    override fun dispatch(context: CoroutineContext, block: Runnable) {
        handler.post(block)
    }
}

HandlerContext继承HandlerDispatcher,而HandlerDispatcher继承MainCoroutineDispatcher

条件都符合,我们直接看isDispatchNeeded方法返回true的逻辑。

  • 首先通过invokeImmediately判断,它代表当前线程是否与自身的线程相同,如果外部使用者能够保证这一点,就可以直接使用Dispatcher.Main.immediate来避免进行线程的切换逻辑。
  • 当然为了保证外部的判断失败,最后也会通过Looper.myLooper() != handler.looper来进行校正。对于Dispatchers.Main这个的handle.looper自然是主线程的looper

如果不能保证则invokeImmediatelyfalse,直接进行线程切换。然后进入dispatch方法,下面是Dispatchers.Maindispatch的处理逻辑。

override fun dispatch(context: CoroutineContext, block: Runnable) {
    handler.post(block)
}

这个再熟悉不过了,因为这个时候的handler.post就是代表向主线程推送消息,此时的block将会在主线程进行调用。

这样线程的切换就完成。

所以综上来看,CoroutineDispatcher为协程提供了一个线程切换的统一判断与执行标准。

  • 首先在协程进行启动的时候通过拦截器的方式进行拦截,对应的方法是interceptContinuation
  • 然后返回一个具有切换线程功能的Continuation,在每次进行resumeWith的时候,内部再通过isDispatchNeeded进行判断当前协程的运行是否需要切换线程。如果需要则调用dispatch进行线程的切换,保证协程的正确运行;
  • 如果我要自定义协程线程的切换逻辑,就可以通过继承于CoroutineDispatcher来实现,将它的核心方法进行自定义即可;
  • 当然,如果你是在Android中使用协程,那基本上是不需要自定义线程的切换逻辑。因为kotlin已经为我们提供了日常所需的Dispatchers