Cracking Kotlin Coroutine (3) - Coroutine Scheduling
1. Coroutine context
The scheduler is essentially the implementation of a coroutine context. Let's introduce the context first.
We mentioned earlier that launch
the function has three parameters, the first parameter is called context , and its interface type is CoroutineContext
, usually the type of context we see is CombinedContext
or EmptyCoroutineContext
, one represents a combination of contexts, and the other represents nothing. Let's look at the interface methods CoroutineContext
of :
@SinceKotlin("1.3")
public interface CoroutineContext {
public operator fun <E : Element> get(key: Key<E>): E?
public fun <R> fold(initial: R, operation: (R, Element) -> R): R
public operator fun plus(context: CoroutineContext): CoroutineContext = ...
public fun minusKey(key: Key<*>): CoroutineContext
public interface Key<E : Element>
public interface Element : CoroutineContext {
public val key: Key<*>
...
}
}
I don’t know if you have noticed, it is simply an index Key
based List
:
|
CoroutineContext
|
List
|
| — | — |
|
get(Key)
|
get(Int)
|
|
plus(CoroutineContext)
|
plus(List)
|
|
minusKey(Key)
|
removeAt(Int)
|
in the table
List.plus(List)
actually refers to the extension methodCollection<T>.plus(elements:Iterable<T>):List<T>
CoroutineContext
As a collection, its elements are the ones seen in the source code Element
, each Element
has one key
, so it can appear as an element, and it is also a sub-interface CoroutineContext
of , so it can also appear as a collection.
Speaking of this, everyone will understand that CoroutineContext
it turns out to be a data structure. If you are familiar with the recursive definition List
of , then itCombinedContext
is easy to understand and , for example, scala's is defined like this:EmptyCoroutineContext
List
sealed abstract class List[+A] extends ... {
...
def head: A
def tail: List[A]
...
}
When the pattern is matched, List(1,2,3,4)
it can be x::y
matched , x
and it is 1, y
so it is List(2,3,4)
.
CombinedContext
The definition of is also very similar:
internal class CombinedContext(
private val left: CoroutineContext,
private val element: Element
) : CoroutineContext, Serializable {
...
}
It's just that it's reversed, the front is a collection, and the back is a single element. coroutineContext
Most of what we access in the coroutine body is this CombinedContext
type, which means that there are many sets of specific context implementations. If we want to find a specific context implementation, we need to use the corresponding Key
to find it, for example:
suspend fun main(){
GlobalScope.launch {
println(coroutineContext[Job]) // "coroutine#1":StandaloneCoroutine{Active}@1ff62014
}
println(coroutineContext[Job]) // null,suspend main 虽然也是协程体,但它是更底层的逻辑,因此没有 Job 实例
}
Here is Job
actually companionobject
a reference to its
public interface Job : CoroutineContext.Element {
/**
* Key for [Job] instance in the coroutine context.
*/
public companion object Key : CoroutineContext.Key<Job> { ... }
...
}
So we can also imitate Thread.currentThread()
to come up with a Job
method to get the current:
suspend inline fun Job.Key.currentJob() = coroutineContext[Job]
suspend fun coroutineJob(){
GlobalScope.launch {
log(Job.currentJob())
}
log(Job.currentJob())
}
We can add some features to the coroutine by specifying the context. A good example is to add a name to the coroutine to facilitate debugging:
GlobalScope.launch(CoroutineName("Hello")) {
...
}
copy
If there are multiple contexts to add, +
just :
GlobalScope.launch(Dispatchers.Main + CoroutineName("Hello")) {
...
}
Dispatchers.Main
is an implementation of the scheduler, don't worry, we'll get to know it shortly.
2. Coroutine interceptor
After spending a lot of time talking about the context, here is a special existence-interceptors.
public interface ContinuationInterceptor : CoroutineContext.Element {
companion object Key : CoroutineContext.Key<ContinuationInterceptor>
public fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T>
...
}
Interceptor is also the implementation direction of a context. Interceptor can control the execution of your coroutine. At the same time, in order to ensure the correctness of its function, the coroutine context collection will always put it at the end. This is really the chosen one. .
Its method of intercepting coroutines is also very simple, because the essence of coroutines is callback + "black magic", and this callback is Continuation
intercepted . Friends who have used OkHttp will be excited immediately. I often use interceptors. OkHttp uses interceptors for caching, logging, and simulating requests. The same is true for coroutine interceptors. The scheduler is implemented based on the interceptor, in other words, the scheduler is a kind of interceptor.
We can define an interceptor and put it in our coroutine context to see what happens.
class MyContinuationInterceptor: ContinuationInterceptor{
override val key = ContinuationInterceptor
override fun <T> interceptContinuation(continuation: Continuation<T>) = MyContinuation(continuation)
}
class MyContinuation<T>(val continuation: Continuation<T>): Continuation<T> {
override val context = continuation.context
override fun resumeWith(result: Result<T>) {
log("<MyContinuation> $result" )
continuation.resumeWith(result)
}
}
We just hit a log line at the callback. Next we take out the use case:
suspend fun main() {
GlobalScope.launch(MyContinuationInterceptor()) {
log(1)
val job = async {
log(2)
delay(1000)
log(3)
"Hello"
}
log(4)
val result = job.await()
log("5. $result")
}.join()
log(6)
}
This is probably the most complicated example we've given so far, but please don't be intimidated by it, it's still pretty simple. We launch
started , specified our own interceptor as the context for it, and then async
started a coroutine async
with in it, which is the same type of function launch
from the function, and they are all called the builder function of the coroutine , the difference is that async
the activated Job
is the actual Deferred
can have a return result, which can await
be obtained through the method.
It is conceivable that result
the value of is Hello. So what is the result of running this program?
15:31:55:989 [main] <MyContinuation> Success(kotlin.Unit) // ①
15:31:55:992 [main] 1
15:31:56:000 [main] <MyContinuation> Success(kotlin.Unit) // ②
15:31:56:000 [main] 2
15:31:56:031 [main] 4
15:31:57:029 [kotlinx.coroutines.DefaultExecutor] <MyContinuation> Success(kotlin.Unit) // ③
15:31:57:029 [kotlinx.coroutines.DefaultExecutor] 3
15:31:57:031 [kotlinx.coroutines.DefaultExecutor] <MyContinuation> Success(Hello) // ④
15:31:57:031 [kotlinx.coroutines.DefaultExecutor] 5. Hello
15:31:57:031 [kotlinx.coroutines.DefaultExecutor] 6
"// ①" is not the content output by the program, it is only marked for the convenience of subsequent explanations.
Everyone may be wondering, didn’t you say Continuation
that is a callback, and there is only one callback call ( await
there), why is the log printed four times?
Don't panic, we will introduce you in order.
First of all, when all coroutines start, there will be Continuation.resumeWith
an operation. This operation is a scheduling opportunity for the scheduler. This is the key to our coroutines having the opportunity to schedule to other threads. This is the case in both ① and ②.
Secondly, delay
it is the suspension point. After 1000ms, the coroutine needs to be scheduled and executed, so there is a log at ③.
Finally, the log at ④ is easy to understand, it is our return result.
Some friends may still have questions. I didn't switch threads in the interceptor. Why is there a thread switching operation starting from ③? The logic of switching threads comes from the fact delay
that on the JVM delay
, ScheduledExcecutor
a delayed task is actually added in a , so thread switching will occur; while in the JavaScript environment, it is based on setTimeout. If it runs on Nodejs, it will delay
not Cut the thread, after all, people are single-threaded.
If we handle the thread switching ourselves in the interceptor, then we have implemented a simple scheduler of our own. If you are interested, you can try it yourself.
Thinking: Can there be more than one interceptor?
3. Scheduler
3.1 Overview
With the previous foundation, our introduction to the scheduler becomes a matter of course.
public abstract class CoroutineDispatcher :
AbstractCoroutineContextElement(ContinuationInterceptor), ContinuationInterceptor {
...
public abstract fun dispatch(context: CoroutineContext, block: Runnable)
...
}
It itself is a subclass of the coroutine context, and at the same time implements the interface of the interceptor, and dispatch
the method will be called interceptContinuation
in , thereby realizing the scheduling of the coroutine. So if we want to implement our own scheduler, we can inherit this class, but usually we use ready-made ones, which are defined Dispatchers
in :
val Default: CoroutineDispatcher
val Main: MainCoroutineDispatcher
val Unconfined: CoroutineDispatcher
The definition of this class involves the support of Kotlin MPP, so you will also see it in the Jvm version val IO:CoroutineDispatcher
. In js and native, there are only the three mentioned above (I am partial to Jvm).
|
|
Jvm
|
Js
|
Native
|
| — | — | — | — |
|
Default
|
Thread Pool
|
main thread loop
|
main thread loop
|
|
Main
|
UI thread
|
Same as Default
|
Same as Default
|
|
Unconfined
|
direct execution
|
direct execution
|
direct execution
|
|
IO
|
Thread Pool
|
–
|
–
|
- IO is only defined on the Jvm. It is based on the thread pool behind the Default scheduler and implements independent queues and limits. Therefore, switching the coroutine scheduler from Default to IO does not trigger thread switching.
- Main is mainly used for UI-related programs, including Swing, JavaFx, and Android on Jvm, and can dispatch coroutines to their respective UI threads.
- Js itself is a single-threaded event loop, which is similar to the UI program on the Jvm.
3.2 Write UI-related programs
The vast majority of Kotlin users are Android developers, and everyone has a relatively large demand for UI development. Let's take a very common scenario, click a button to do some asynchronous operations and then call back to refresh the UI:
getUserBtn.setOnClickListener {
getUser { user ->
handler.post {
userNameView.text = user.name
}
}
}
We simply give the declaration of getUser
the function :
typealias Callback = (User) -> Unit
fun getUser(callback: Callback){
...
}
Since getUser
the function needs to be switched to other threads for execution, the callback is usually called in this non-UI thread, so in order to ensure that the UI is refreshed correctly, we need to use handler.post
to switch to the UI thread. The above writing method is our oldest writing method.
Then came RxJava, and things started to get interesting:
fun getUserObservable(): Observable<User> {
return Observable.create<User> { emitter ->
getUser {
emitter.onNext(it)
}
}
}
So the button click event can be written as follows:
getUserBtn.setOnClickListener {
getUserObservable()
.observeOn(AndroidSchedulers.mainThread())
.subscribe { user ->
userNameView.text = user.name
}
}
In fact, RxJava's performance in thread switching is very good, and it is exactly the same. Many people even use it just for the convenience of thread switching!
So now we transition this code to the coroutine writing method:
suspend fun getUserCoroutine() = suspendCoroutine<User> {
continuation ->
getUser {
continuation.resume(it)
}
}
On button click, we can:
getUserBtn.setOnClickListener {
GlobalScope.launch(Dispatchers.Main) {
userNameView.text = getUserCoroutine().name
}
}
You can also use the View.onClick extension in anko-coroutines, so we don't need to
launch
start . Regarding Anko's support for coroutines, we will arrange an article to introduce them later.
Here is something you haven’t seen before. suspendCoroutine
This method does not help us start the coroutine. It runs in the coroutine and helps us get Continuation
the instance , that is, get the callback, which is convenient for us to call it later. The resume
or resumeWithException
to return a result or throw an exception.
If you call repeatedly
resume
orresumeWithException
get a coinIllegalStateException
, think about why.
Compared with the previous RxJava approach, you will find that this code is actually very easy to understand, and you will even find that the usage scenarios of coroutines are so similar to RxJava. Here we use Dispatchers.Main
to ensure launch
that the coroutine started by is always scheduled to the UI thread when scheduling, so let's take a look at the specific implementation Dispatchers.Main
of .
On the Jvm, Main
the implementation of is also more interesting:
internal object MainDispatcherLoader {
@JvmField
val dispatcher: MainCoroutineDispatcher = loadMainDispatcher()
private fun loadMainDispatcher(): MainCoroutineDispatcher {
return try {
val factories = MainDispatcherFactory::class.java.let { clz ->
ServiceLoader.load(clz, clz.classLoader).toList()
}
factories.maxBy { it.loadPriority }?.tryCreateDispatcher(factories)
?: MissingMainCoroutineDispatcher(null)
} catch (e: Throwable) {
MissingMainCoroutineDispatcher(e)
}
}
}
In Android, the coroutine framework registers AndroidDispatcherFactory
so that is Main
finally assigned an instance HandlerDispatcher
of . If you are interested, you can check the source code implementation of kotlinx-coroutines-android.
Note that in the previous implementation of RxJava and coroutines, we did not consider exceptions and cancellations. The topic of exceptions and cancellation will be covered in detail in a later article.
3.3 Schedulers bound to arbitrary threads
The purpose of the scheduler is to cut threads. Don't think that I will randomly call it according to my mood when I am dispatch
in , then you are harming yourself (don't be afraid of your jokes, I really wrote such code, just for entertainment). Then the problem is simple, as long as we provide threads, the scheduler should be easily created:
suspend fun main() {
val myDispatcher= Executors.newSingleThreadExecutor{ r -> Thread(r, "MyThread") }.asCoroutineDispatcher()
GlobalScope.launch(myDispatcher) {
log(1)
}.join()
log(2)
}
The output information indicates that the coroutine is running on our own thread.
16:10:57:130 [MyThread] 1
16:10:57:136 [MyThread] 2
But please note that since this thread pool is created by ourselves, we need to close it at the right time, otherwise:
We can actively close the thread pool or call:
myDispatcher.close()
To end its life cycle, run the program again and it will exit normally.
Of course, some people will say that the threads in the thread pool you created are not daemon, so the Jvm will not stop running when the main thread ends. You are right, but what should be released should be released in time. If you only use this scheduler for a short time in the entire life cycle of the program, won't there be thread leaks if you don't close its corresponding thread pool all the time? That's embarrassing.
Kotlin coroutine designers are also very afraid that people will not notice this, and they deliberately abandoned two APIs and opened an issue saying that we want to redo this set of APIs. Who are these two poor guys?
Two abandoned APIs for creating schedulers based on thread pools
fun newSingleThreadContext(name: String): ExecutorCoroutineDispatcher
fun newFixedThreadPoolContext(nThreads: Int, name: String): ExecutorCoroutineDispatcher
These two can be very convenient to create a scheduler bound to a specific thread, but the overly concise API seems to make people forget its risk. Kotlin has never liked to do such unclear things, so you should construct the thread pool by yourself as in the example in this section, so that you forget to close it anyway and you can’t blame others (hahaha).
In fact, running coroutines on multiple threads, the threads are always cut like this is not very lightweight. For example, the following example is quite scary:
Executors.newFixedThreadPool(10)
.asCoroutineDispatcher().use { dispatcher ->
GlobalScope.launch(dispatcher) {
log(1)
val job = async {
log(2)
delay(1000)
log(3)
"Hello"
}
log(4)
val result = job.await()
log("5. $result")
}.join()
log(6)
}
Except delay
for an unavoidable thread switch, the continuation operations ( Continuation.resume
) at the suspension points of other coroutines will cut threads:
16:28:04:771 [pool-1-thread-1] 1
16:28:04:779 [pool-1-thread-1] 4
16:28:04:779 [pool-1-thread-2] 2
16:28:05:790 [pool-1-thread-3] 3
16:28:05:793 [pool-1-thread-4] 5. Hello
16:28:05:794 [pool-1-thread-4] 6
If our thread pool only opens 1 thread, then all output here will be printed in this only thread:
16:40:14:685 [pool-1-thread-1] 1
16:40:14:706 [pool-1-thread-1] 4
16:40:14:710 [pool-1-thread-1] 2
16:40:15:723 [pool-1-thread-1] 3
16:40:15:725 [pool-1-thread-1] 5. Hello
16:40:15:725 [pool-1-thread-1] 6
Comparing the two, in the case of 10 threads, the number of thread switches is at least 3 times, while in the case of 1 thread, it only needs to delay
be executed once after 1000ms. Just two more thread switches, how much impact will it have? I ran the loop 100 times for two different cases on my own 2015 mbp and got the following average times:
|
Number of threads
|
10
|
1
|
| — | — | — |
|
Time-consuming ms
|
1006.00
|
1004.97
|
Note that for the fairness of the test, a warm-up has been done before running the loop 100 times to ensure that all classes have been loaded. Test results are for reference only.
That is to say, two more thread switches can take an average of 1ms more time-consuming. The code in the production environment will of course be more complicated. If you use the thread pool to schedule in this way, the result can be imagined.
In fact, usually we only need to process our own business logic in one thread, and only some time-consuming IO needs to be switched to the IO thread for processing, so a good practice can refer to the scheduler corresponding to the UI, and define the scheduler through the thread pool by yourself There is nothing wrong with the approach itself, but it is best to use only one thread, because multi-threading has thread safety issues in addition to the overhead of thread switching mentioned above.
3.4 Thread Safety Issues
The concurrency model of Js and Native is different from Jvm. Jvm exposes the thread API to users, which also makes the scheduling of coroutines more flexible for users to choose. More freedom means more cost. What we need to understand when writing coroutine code on Jvm is that thread safety issues still exist between different coroutines of the scheduler.
A good practice, as we mentioned in the previous section, is to try to control your own logic within one thread, which saves the cost of thread switching on the one hand, and avoids thread safety issues on the other hand, which is the best of both worlds.
If you use concurrency tools such as locks in the coroutine code, it will increase the complexity of the code. My suggestion for this is that you try to avoid referencing variable variables in the external scope when writing coroutine code. Use parameter passing instead of references to global variables.
The following is an example of a mistake, which is easy for everyone to figure out:
suspend fun main(){
var i = 0
Executors.newFixedThreadPool(10)
.asCoroutineDispatcher().use { dispatcher ->
List(1000000) {
GlobalScope.launch(dispatcher) {
i++
}
}.forEach {
it.join()
}
}
log(i)
}
Output result:
16:59:28:080 [main] 999593
4. How to schedule the suspend main function?
In the previous article, we mentioned that suspend main will start a coroutine. The coroutines in our example are all its sub-coroutines, but how did this outermost coroutine come about?
Let's give an example first:
suspend fun main() {
log(1)
GlobalScope.launch {
log(2)
}.join()
log(3)
}
It is equivalent to writing the following:
fun main() {
runSuspend {
log(1)
GlobalScope.launch {
log(2)
}.join()
log(3)
}
}
Then why runSuspend
do is sacred? It is a method of the Kotlin standard library, note that it is not in kotlinx.coroutines, it actually belongs to a lower-level API.
internal fun runSuspend(block: suspend () -> Unit) {
val run = RunSuspend()
block.startCoroutine(run)
run.await()
}
And RunSuspend
here is Continuation
the implementation of :
private class RunSuspend : Continuation<Unit> {
override val context: CoroutineContext
get() = EmptyCoroutineContext
var result: Result<Unit>? = null
override fun resumeWith(result: Result<Unit>) = synchronized(this) {
this.result = result
(this as Object).notifyAll()
}
fun await() = synchronized(this) {
while (true) {
when (val result = this.result) {
null -> (this as Object).wait()
else -> {
result.getOrThrow() // throw up failure
return
}
}
}
}
}
Its context is empty, so the coroutine started by suspend main does not have any scheduling behavior.
Through this example, we can know that actually starting a coroutine only needs a lambda expression. When Kotlin 1.1 was first released, I wrote a series of tutorials based on the standard library API. Later, I found that the API of the standard library may not really be used by us, so just take a look.
The above codes are decorated in the standard library
internal
, so we cannot use them directly. However, you can copy the content of RunSuspend.kt to your project, so that you can use it directly, and thevarresult:Result<Unit>?=null
may report an error, it doesn't matter,privatevarresult:Result<Unit>?=null
just change it to .
5. Summary
In this article, we introduced the coroutine context, introduced the interceptor, and finally led to our scheduler. So far, we have not talked about exception handling, coroutine cancellation, Anko's support for coroutines, etc. Yes, if you have any topics related to coroutines that you want to know, you can leave a message~
More Kotlin learning materials can be scanned for free!
The Kotlin Getting Started Tutorial Guide
Chapter 1 Kotlin Getting Started Tutorial Guide
● Preface
Chapter 2 Overview
● Server-side development with Kotlin
● Android development with Kotlin
● Kotlin JavaScript Overview
● Kotlin/Native for native development
● Coroutines for scenarios such as asynchronous programming
● New features in Kotlin 1.1
● New features in Kotlin 1.2
● New features in Kotlin 1.3
Chapter 3 begins
● Basic syntax
● Idioms
● Coding Standards
Chapter 4 Basics
● Basic types
● package
● Control flow: if, when, for, while
● Back and jump
Chapter 5 Classes and Objects
● Classes and Inheritance
● Attributes and fields
● interface
● Visibility modifiers
● Extension
● data class
● Sealed
● Generics
● Nested classes and inner classes
● Enumeration class
● Object expressions and object declarations
● Inline classes
● commission
delegated property