“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,kotlin
How exactly does a coroutine switch threads?
The specific contents are as follows:
1. Pre-knowledge
1.1 CoroutineScope
What 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 GlobalScope
withViewModelScope
What’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.IO
Etc.
In effect, the function of the scheduler is to control the thread on which the coroutine runs
Structurally,Dispatchers
Is the parent classContinuationInterceptor
And then inherit fromCoroutineContext
Their class structure relations are as follows:
And that’s whyDispatchers
Can join theCoroutineContext
And 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 launch
Method 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.interepted
Is an extension method that will eventually callContinuationImpl.intercepted
methods
In 2.intercepted
Make use ofCoroutineContext
To 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 bodyContinuation
The incomingDispatchedContinuation
That’s actually what we use hereDecorator pattern
To realize the enhancement of functions
It’s actually pretty obvious here, throughDispatchedContinuation
Decorates the original coroutine inDispatchedContinuation
In 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 usinghandler
Switch to the main thread
If you useDispatcers.IO
Same thing, but with a thread pool switch
As shown above, this is actually a decorative pattern
1. CallCoroutinDispatcher.dispatch
Method switch thread
2. Call after the switchover is completeDispatchedTask.run
Method that executes the real coroutine body
3 delay
How 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. withContext
How 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