“This article has participated in the good article call order activity, click to see: back end, big front end double track submission, 20,000 yuan prize pool for you to challenge!”

preface

I have made a brief introduction to coroutine before, and answered the question of what coroutine is. If you are interested in learning coroutine, you can know: what is coroutine? 2. Kotlin coroutines can write asynchronous code synchronously to automatically manage thread switching

Which brings us to the main point of this article,kotlinHow exactly does a coroutine switch threads?

The specific contents are as follows:

1. Pre-knowledge

1.1 CoroutineScopeWhat is it?

CoroutineScope is the scope in which coroutines run, and its source code is simple

public interface CoroutineScope {
    public val coroutineContext: CoroutineContext
}
Copy the code

Can see CoroutineScope code is very simple, the main function is to provide CoroutineContext, coroutines running context We have common implementation GlobalScope LifecycleScope, ViewModelScope, etc

1.2 GlobalScopewithViewModelScopeWhat’s the difference?

public object GlobalScope : CoroutineScope {
    ** * returns [EmptyCoroutineContext]. */
    override val coroutineContext: CoroutineContext
        get() = EmptyCoroutineContext
}

public val ViewModel.viewModelScope: CoroutineScope
    get() {
        val scope: CoroutineScope? = this.getTag(JOB_KEY)
        if(scope ! =null) {
            return scope
        }
        return setTagIfAbsent(
            JOB_KEY,
            CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
        )
    }
Copy the code

The code for both is quite simple, as you can see from the above: 1.GlobalScope returns an empty implementation of CoroutineContext; 2.ViewModelScope adds a Job and Dispatcher to CoroutineContext

Let’s start with a simple piece of code

	fun testOne(a){
		GlobalScope.launch {
            print("1:" + Thread.currentThread().name)
            delay(1000)
            print("2:" + Thread.currentThread().name)
        }
	}
	// The output is defaultdispatcher-worker-1
    fun testTwo(a){
        viewModelScope.launch {
            print("1:" + Thread.currentThread().name)
            delay(1000)
            print("2:" + Thread.currentThread().name)
        }
    }
    // Print the result as: main
Copy the code

The above two kinds of Scope after startup coroutines, print the current thread is different, one is a thread in the thread pool, one is the main thread This is because the ViewModelScope added in CoroutineContext Dispatchers. Main. The immediate cause

We can conclude that coroutines control thread switching through Dispatchers

1.3 What is a scheduler?

In terms of usage, the scheduler is what we useDispatchers.Main.Dispatchers.Default.Dispatcher.IOEtc.

In effect, the function of the scheduler is to control the thread on which the coroutine runs

Structurally,DispatchersIs the parent classContinuationInterceptorAnd then inherit fromCoroutineContext

Their class structure relations are as follows:



And that’s whyDispatchersCan join theCoroutineContextAnd support+Operator to accomplish the increment

1.4 What is an interceptor

ContinuationInterceptor is a coroutine interceptor. Take a look at the interface first

interface ContinuationInterceptor : CoroutineContext.Element {
    ContinuationInterceptor Key in CoroutineContext
    companion object Key : CoroutineContext.Key<ContinuationInterceptor>
    /** * Block continuation */
    fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T>

    / /...
}
Copy the code

1. The Key of the interceptor is singleton, so when you add multiple interceptors, only one 2 will take effect. We all know that a Continuation is calling its Continuation#resumeWith() method, executing a block of code from its suspend modified function. If we intercepted it earlier, could we have done something else? This is how the scheduler switches threads

Above we have introduced is through Dispatchers to specify the thread running coroutine, through interceptContinuation in the coroutine recovery before interception, so as to switch the thread with these front knowledge, let’s look at the specific process of coroutine start, clear coroutine switch thread source specific implementation

2. Coroutine thread switch source analysis

2.1 launchMethod resolution

Let’s first look at how the coroutine is started, what parameters are passed

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
        StandaloneCoroutine(newContext, active = true)
    coroutine.start(start, coroutine, block)
    return coroutine
}
Copy the code

The CoroutinStart initiator, which is an enumeration class that defines different methods for starting the coroutine. The DEFAULT is Coroutinestart.default. The block is the body of the coroutine we pass in, and the actual code to execute

This code does two main things: 1. Combines the new CoroutineContext 2. Create another Continuation

2.1.1 Combining newCoroutineContext

public actual fun CoroutineScope.newCoroutineContext(context: CoroutineContext): CoroutineContext {
    val combined = coroutineContext + context
    val debug = if (DEBUG) combined + CoroutineId(COROUTINE_ID.incrementAndGet()) else combined
    return if(combined ! == Dispatchers.Default && combined[ContinuationInterceptor] ==null)
        debug + Dispatchers.Default else debug
}
Copy the code

The following information can be extracted from the above: 1. The context passed in the launch method will be combined with the context in the CoroutineScope 2. If there is no interceptor in combined, a Default interceptor is passed, dispatchers.default. This explains why there is a Default thread switching effect if we do not pass in an interceptor

2.1.2 Creating oneContinuation

val coroutine = if (start.isLazy)
        LazyStandaloneCoroutine(newContext, block) else
        StandaloneCoroutine(newContext, active = true)
    coroutine.start(start, coroutine, block)
Copy the code

By default, we create a StandloneCoroutine. The remarkable thing is that this coroutine is actually the complete, the successful callback of our coroutine body, not the coroutine body itself and then calls coroutine. Start, indicating that the coroutine has started

2.2 Coroutine startup

public fun <R> start(start: CoroutineStart, receiver: R, block: suspend R. () - >T) {
    initParentJob()
    start(block, receiver, this)}Copy the code

The coroutine is then started by calling start of CoroutineStart, which is called coroutinestart.default by Default

After a series of calls, finally arrived at:

internal fun <R, T> (suspend (R) -> T).startCoroutineCancellable(receiver: R, completion: Continuation<T>) =
    runSafely(completion) {
        // Wrap another layer of Coroutine
        createCoroutineUnintercepted(receiver, completion)
            // Intercept if necessary
            .intercepted()
            // Call the resumeWith method
            .resumeCancellableWith(Result.success(Unit))}Copy the code

Here is the coroutine to start the core code, although relatively short, but includes three steps: 1. Create a coroutine Continuation 2. Create an intercepting Continuation, DispatchedContinuation 3. Perform DispatchedContinuation resumeWith method

2.3 Creating a coroutine bodyContinuation

Call createCoroutineUnintercepted, will suspend our coroutines model that block into a Continuation, it is SuspendLambda, Inherited from ContinuationImpl createCoroutineUnintercepted method can’t find the specific implementation in the source code, but if you read the coroutines body code compiled can see the real implementation Details: bytecode compilation

2.4 createDispatchedContinuation

public actual fun <T> Continuation<T>.intercepted(a): Continuation<T> =
    (this as? ContinuationImpl)? .intercepted() ?:this

//ContinuationImpl
public fun intercepted(a): Continuation<Any? > = intercepted ? : (context[ContinuationInterceptor]? .interceptContinuation(this) ?: this)
                .also { intercepted = it }     

//CoroutineDispatcher
public final override fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T> =
      DispatchedContinuation(this, continuation)           
Copy the code

The following information can be extracted from the above

1.intereptedIs an extension method that will eventually callContinuationImpl.interceptedmethods

In 2.interceptedMake use ofCoroutineContextTo get the current interceptor

3. Because the current interceptor isCoroutineDispatcher, so it will eventually return oneDispatchedContinuation, we actually use it to achieve thread switching

4. We will coroutine bodyContinuationThe incomingDispatchedContinuationThat’s actually what we use hereDecorator patternTo realize the enhancement of functions



It’s actually pretty obvious here, throughDispatchedContinuationDecorates the original coroutine inDispatchedContinuationIn the scheduler processing thread switching, does not affect the original logic, to achieve the enhancement of the function

2.5 Interception

//DispatchedContinuation inline fun resumeCancellableWith( result: Result<T>, noinline onCancellation: ((cause: Throwable) -> Unit)? ) { val state = result.toState(onCancellation) if (dispatcher.isDispatchNeeded(context)) { _state = state resumeMode = MODE_CANCELLABLE dispatcher.dispatch(context, this) } else { executeUnconfined(state, MODE_CANCELLABLE) { if (! resumeCancelled(state)) { resumeUndispatchedWith(result) } } } }Copy the code

A resumeCancellableWith method will be called with DispatchedContinuation when started. If you need to switch threads, call the dispatcher.dispatcher method, where dispatcher is 2 retrieved from CoroutineConext. If you don’t need to switch threads, just run the original thread

2.5.2 Concrete implementation of scheduler

CoroutineDispatcher is taken out by CoroutineContext, which is also the embodiment of CoroutineContext. CoroutineDispater officially provides four implementations: Dispatchers. The Main, Dispatchers. IO, Dispatchers. Default, Dispatchers. The Unconfined together we simply look at the Dispatchers. The realization of the Main

internal class HandlerContext private constructor(
    private val handler: Handler,
    private valname: String? .private val invokeImmediately: Boolean
) : HandlerDispatcher(), Delay {
    public constructor(
        handler: Handler,
        name: String? = null
    ) : this(handler, name, false)

    / /...

    override fun dispatch(context: CoroutineContext, block: Runnable) {
        // Use the Handler of the main thread to execute the task
        handler.post(block)
    }
}
Copy the code

As you can see, it’s just usinghandlerSwitch to the main thread

If you useDispatcers.IOSame thing, but with a thread pool switch



As shown above, this is actually a decorative pattern

1. CallCoroutinDispatcher.dispatchMethod switch thread

2. Call after the switchover is completeDispatchedTask.runMethod that executes the real coroutine body

3 delayHow do you switch threads?

We know that the delay function suspends and then waits for a certain period of time before resuming. You can imagine that this should also involve thread switching, how to achieve specific?

public suspend fun delay(timeMillis: Long) {
    if (timeMillis <= 0) return // don't delay
    return suspendCancellableCoroutine sc@ { cont: CancellableContinuation<Unit> ->
        // if timeMillis == Long.MAX_VALUE then just wait forever like awaitCancellation, don't schedule.
        if (timeMillis < Long.MAX_VALUE) {
            cont.context.delay.scheduleResumeAfterDelay(timeMillis, cont)
        }
    }
}

internal val CoroutineContext.delay: Delay get() = get(ContinuationInterceptor) as? Delay ? : DefaultDelayCopy the code

Dealy code is also very simple, from the above can be derived the following information: delay switching is also implemented by interceptors, built-in interceptors also implement the delay interface. Let’s look at a concrete implementation

internal class HandlerContext private constructor(
    private val handler: Handler,
    private valname: String? .private val invokeImmediately: Boolean
) : HandlerDispatcher(), Delay {
    override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation<Unit>) {
        // Use the Handler of the main thread to delay the execution of the task, and put the completed continuation in the task to execute
        val block = Runnable {
            with(continuation) { resumeUndispatched(Unit) }
        }
        handler.postDelayed(block, timeMillis.coerceAtMost(MAX_DELAY))
        continuation.invokeOnCancellation { handler.removeCallbacks(block) }
    }

    / /..
}
Copy the code

1. We can see that the effect of delay is also realized through handler.postDelayed 2. When the time is up, the coroutine 3 is restored using the resumeUndispatched method. If we use dispatcher. IO, the effect is the same, except that the delay effect is achieved by switching threads

4. withContextHow do you switch threads?

In coroutines, we can easily switch threads withContext and write asynchronous code synchronously, which is one of the main advantages of kotin coroutines

    fun test(a){
        viewModelScope.launch(Dispatchers.Main) {
            print("1:" + Thread.currentThread().name)
            withContext(Dispatchers.IO){
                delay(1000)
                print("2:" + Thread.currentThread().name)
            }
            print("3:" + Thread.currentThread().name)
        }
    }
    // Output main, defaultDispatcher-worker-1,main at 1, 2 and 3 respectively
Copy the code

You can see that this code does a thread switch and then a thread switch back, and we can ask two questions: 1. How does a thread switch withContext? 2. How does the thread switch back to Dispatchers.Main after the coroutine body within withContext ends?

public suspend fun <T> withContext(
    context: CoroutineContext,
    block: suspend CoroutineScope. () - >T
): T {  
    return suspendCoroutineUninterceptedOrReturn sc@ { uCont ->
        // Create a new context
        val oldContext = uCont.context
        val newContext = oldContext + context
        ....
        // Use the new Dispatcher to cover the outer layer
        val coroutine = DispatchedCoroutine(newContext, uCont)
        coroutine.initParentJob()
        // The DispatchedCoroutine is passed as complete
        block.startCoroutineCancellable(coroutine, coroutine)
        coroutine.getResult()
    }
}

private class DispatchedCoroutine<in T>(
    context: CoroutineContext,
    uCont: Continuation<T>
) : ScopeCoroutine<T>(context, uCont) {
	// The callback is called when complete
    override fun afterCompletion(state: Any?). {
        afterResume(state)
    }

    override fun afterResume(state: Any?). {
        //uCont is the parent coroutine, and context is still the original context, so you can switch back to the original thread
        uCont.intercepted().resumeCancellableWith(recoverResult(state, uCont))
    }
}
Copy the code

This code is actually very simple, also can refine the following information 1. WithContext is actually a layer Api encapsulation, the last call to startCoroutineCancellable, it just like the process behind the launch, we will not continue with the 2. 3. The DispatchedCoroutine is passed as complete to the coroutine body. 4. The uCont passed in the DispatchedCoroutine is the parent coroutine, and its interceptor is still the outer interceptor, so it switches back to the original thread

conclusion

This article mainly answers the kotlin coroutine in the end is how to switch the thread of this question, and analyzed the source code is simple to say mainly includes the following steps: 1. Add Dispatcher to CoroutineContext, specifying the coroutine 2 to run. The suspend block is created as a Continuation at startup and intercepted is called to generate a DispatchedContinuation. After calling Dispatcher to complete the thread switching task, the coroutine decorated with resume will execute the code inside the coroutine

It may seem like a lot of code, but the real idea is actually quite simple, and that’s probably what design mode is for

The resources

Kotlin coroutine crack kotlin coroutine (3) – coroutine scheduling chapter