
What is a coroutine
The official introduction to Kotlin coroutines: Coroutines basics states:
A coroutine is an instance of suspendable computation. It is conceptually similar to a thread, in the sense that it takes a block of code to run that works concurrently with the rest of the code. However, a coroutine is not bound to any particular thread . It may suspend its execution in one thread and resume in another one.
Code language: JavaScript (javascript)
Coroutines can be thought of as light-weight threads, but there is a number of important differences that make their real-life usage very different from threads.
Code language: JavaScript (javascript)
In my opinion, coroutines are Kotlin’s encapsulation of threads and Handler APIs, and a solution to elegantly handle asynchronous tasks. Coroutines can be switched back and forth between different threads, eliminating the need to write callbacks in various time-consuming operations.
Introduction to the suspend keyword
Before we officially start talking about coroutines, let’s first introduce the Kotlin keyword suspend
, suspend
is used to temporarily suspend the current thread, and automatically switch back to the original thread later,suspend
can be used for decoration Ordinary method, indicates that this method is a time-consuming operation and can only be called in the context of a coroutine, or in another suspend method.
When the code is executed in a method modified by the suspend
keyword, the execution of the current thread will be suspended first. It should be noted that the suspension here is non-blocking (that is, it will not block the current thread), and then it will go to Execute the method with the suspend modification first. When the method is executed, the thread that has just been suspended will continue to be executed
In addition, it should be noted that suspend
itself will not have the effect of thread suspension or thread switching, so what is its real role? It is more of a reminder, indicating that this is a time-consuming method and cannot be executed directly. It needs to be called in a coroutine, so we need to add suspend to it when writing a time-consuming method. keyword, which can effectively avoid the situation where we call time-consuming operations in the main thread and cause the application to freeze.
suspend fun getUserName(userId: Int): String {
delay(20000)
return "Android CoroutineTest"
}
Code language: JavaScript (javascript)
Integrated coroutines
dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2'
}
Code language: JavaScript (javascript)
Three common operation symbols for coroutines
- runBlocking: as the name implies, it will block the execution of the current thread
- launch: will not block the current thread, but will execute code asynchronously
- async: similar to launch, the only difference is that it can have a return value
runBlocking usage
Let’s test it with the code
println("Test start " + (Thread.currentThread() == Looper.getMainLooper().thread))
runBlocking {
println("Test delay start " + (Thread.currentThread() == Looper.getMainLooper().thread))
delay(20000)
println("Test delay end")
}
println("Test end")
Code language: JavaScript (javascript)
result:
Test start true
Test delay start true
Test delay end
Test end
Code language: JavaScript (javascript)
runBlocking
runs on the main thread, so it can be seen that, like its name, it will really block the current thread, and the code outside runBlocking will only be executed after the code inside runBlocking is executed.
At this point, you may have a question. Since the thread is blocked, is it the same for me to write directly in code? Why use runBlocking
?
answering this question is very simple, we just need to find a place without coroutines, add delay(20000)
then you will receive a warning from the compiler:
Suspend function 'delay' should be called only from a coroutine or another suspend function
Code language: JavaScript (javascript)
If we call a method modified by suspend, then it must be called within the coroutine, so runBlocking is not useless
launch usage
It is the main thread when testing, but it will become a sub-thread when it is launched. This effect is similar to new Thread(). The most different from runBlocking
is that launch
has no concept of execution order
println("Test start " + (Thread.currentThread() == Looper.getMainLooper().thread))
GlobalScope.launch {
println("Test delay start " + (Thread.currentThread() == Looper.getMainLooper().thread))
delay(20000)
println("Test delay end")
}
println("Test end")
Code language: JavaScript (javascript)
Result:
Test start true
Test end
Test delay start false
Test delay end
Code language: JavaScript (javascript)
async usage
println("Test start " + (Thread.currentThread() == Looper.getMainLooper().thread))
GlobalScope.async {
println("Test delay start " + (Thread.currentThread() == Looper.getMainLooper().thread))
delay(20000)
println("Test delay end")
}
println("Test end")
Code language: JavaScript (javascript)
Result:
Test start true
Test end
Test delay start false
Test delay end
Code language: JavaScript (javascript)
Isn’t this the same result as launch
? So what’s the difference between the two? Let’s first look at a test code
println("Test start " + (Thread.currentThread() == Looper.getMainLooper().thread))
val async = GlobalScope.async {
println("Test delay start " + (Thread.currentThread() == Looper.getMainLooper().thread))
delay(20000)
println("Test delay end")
return@async "async"
}
println("Test end")
println("Test return:" + async.await())
Code language: JavaScript (javascript)
Result:
Test start true
Test end
Test delay start false
Test delay end
Test return:async
Code language: JavaScript (javascript)
See if you understand here, that there is still a difference between async
and launch
. async
can have a return value, which can be obtained through its await
method. It should be noted that this method can only be used in the operator of the coroutine or the method modified by suspend. can be called in.
coroutine thread scheduler
there are four types of thread schedulers in total:
- Dispatchers.Main: The main thread scheduler, as the name suggests, will be executed in the main thread
- Dispatchers.IO: Worker thread scheduler, as its name suggests, will execute in child threads
- Dispatchers.Default: The default scheduler, this is used when the scheduler is not set. After testing, the effect is basically the same as
Dispatchers.IO
- Dispatchers.Unconfined: There is no specified scheduler. According to the current execution environment, it will be executed on the current thread. Another point to note is that because the current thread is directly executed
Coroutine has something similar to RxJava thread scheduling, Try it with launch first
println("Test start " + (Thread.currentThread() == Looper.getMainLooper().thread))
GlobalScope.launch(Dispatchers.Main) {
println("Test delay start " + (Thread.currentThread() == Looper.getMainLooper().thread))
delay(20000)
println("Test delay end")
}
println("Test end")
Code language: JavaScript (javascript)
Result:
Test start true
Test end
Test delay start true
Test delay end
Code language: JavaScript (javascript)
Using the thread scheduler can control which thread the coroutine executes on. This is mainly due to Dispatchers (scheduler). If not specify the scheduler of the launch statement, it must be executed in the child thread, but when specify Dispatchers.MainAfter
, it becomes executed in the main thread.
Thread scheduling can happen not only in the launch
and async
of the coroutine but also inside the coroutine, for example:
println("Test start " + (Thread.currentThread() == Looper.getMainLooper().thread))
GlobalScope.launch(Dispatchers.Main) {
withContext(Dispatchers.IO) {
println("Test main thread? " + (Thread.currentThread() == Looper.getMainLooper().thread))
}
println("Test delay start " + (Thread.currentThread() == Looper.getMainLooper().thread))
delay(20000)
println("Test delay end")
}
println("Test end")
Code language: JavaScript (javascript)
Result:
Test start true
Test end
Test main thread? false
Test delay start true
Test delay end
Code language: JavaScript (javascript)
Judging from the printed log, the withContext
function is to suspend the current thread. Only when the code in withContext
is executed, then the execution of the current thread resume.
It should be noted that withContext
can only be called in the operator of the coroutine or the method modified by suspend
. There are two specific usages, as follows:
suspend fun getUserName(userId: Int): String {
return withContext(Dispatchers.IO) {
delay(20000)
return@withContext "Android CoroutineTest"
}
}
suspend fun getUserName(userId: Int): String = withContext(Dispatchers.IO) {
delay(20000)
return@withContext "Android CoroutineTest"
}
Code language: JavaScript (javascript)
Coroutine startup mode
- CoroutineStart.DEFAULT: default mode, will execute immediately
- CoroutineStart.LAZY: lazy loading mode, will not execute, only manually call the start method of the coroutine will execute
- CoroutineStart.ATOMIC: Atomic mode, similar to
CoroutineStart.DEFAULT
, but the coroutine cannot be canceled before it starts executing. It should be noted that this is an experimental API and may change later. - CoroutineStart.UNDISPATCHED: If the mode is not specified, the coroutine will be executed immediately. After practice, it will cause the originally set thread scheduler to fail. It will be executed on the original thread at the beginning, similar to
Dispatchers. Unconfined
, but once the coroutine is suspended, Then resume execution, it will become the thread set by the thread scheduler to execute on the thread.
Having said that, how do you use it? For example CoroutineStart.LAZY
, the specific usage is as follows:
val job = GlobalScope.launch(Dispatchers.Default, CoroutineStart.LAZY) {
}
job.start()
Code language: PHP (php)
Here is an additional introduction to the usage of several functions of the coroutine:
- job.start: start the coroutine, except in lazy mode, the coroutine does not need to be started manually
- job.cancel: cancel a coroutine, it can be cancelled, but it will not take effect immediately, there is a certain delay
- job.join: wait for the coroutine to finish executing, this is a time-consuming operation and needs to be used in the coroutine
- job.cancelAndJoin: wait for the coroutine to complete and then cancel
Sets the execution timeout of coroutine
When the coroutine is executing, we can set an execution time for it. If the execution time exceeds the specified time, the coroutine will automatically stop. The specific usage is as follows:
GlobalScope.launch() {
try {
withTimeout(300) {
repeat(5) { i ->
println("Test output " + i)
delay(100)
}
}
} catch (e: TimeoutCancellationException) {
println("Test Timeout")
}
}
Code language: JavaScript (javascript)
Result:
23:52:48.415 System.out: Test output 0
23:52:48.518 System.out: Test output 1
23:52:48.618 System.out: Test output 2
23:52:48.715 System.out: Test Timeout
Code language: CSS (css)
In addition to the usage of withTimeout
, there is another method: withTimeoutOrNull
, the biggest difference between this and withTimeout
is that it will not throw TimeoutCancellationException
to the coroutine after a timeout, but return null directly. If there is no timeout, it will return the coroutine body The results inside are as follows:
GlobalScope.launch() {
val result = withTimeoutOrNull(300) {
repeat(5) { i ->
println("Test output " + i)
delay(100)
}
return@withTimeoutOrNull "Done"
}
println("Test output " + result)
}
Code language: JavaScript (javascript)
Result:
23:56:02.462 System.out: Test output 0
23:56:02.569 System.out: Test output 1
23:56:02.670 System.out: Test output 2
23:56:02.761 System.out: Test output null
Code language: CSS (css)
If we change the timeout from 300 milliseconds to 1000 milliseconds, then the coroutine in this case will definitely not time out, and the final printed result is as follows:
23:59:37.288 System.out: Test output 0
23:59:37.390 System.out: Test output 1
23:59:37.491 System.out: Test output 2
23:59:37.591 System.out: Test output 3
23:59:37.692 System.out: Test output 4
23:59:37.793 System.out: Test output Done
Code language: CSS (css)
Coroutine lifecycle control
If we use the GlobalScope
class directly in the code to operate the coroutine, there will be a yellow warning:
This is a delicate API and its use requires care. Make sure you fully read and understand the documentation of the declaration that is marked as a delicate API.
Code language: JavaScript (javascript)
Let’s take a look at Google’s introduction to the GlobalScope class:
A global [CoroutineScope] not bound to any job.
Global scope is used to launch top-level coroutines which are operating on the whole application lifetime and are not cancelled prematurely.
Active coroutines launched in GlobalScopedo not keep the process alive. They are like daemon threads.
This is a delicate API. It is easy to accidentally create resource or memory leaks when GlobalScopeis used. A coroutine launched in ```GlobalScopeis``` not subject to the principle of structured concurrency, so if it hangs or gets delayed due to a problem (eg due to a slow network), it will stay working and consuming resources.
Code language: JavaScript (javascript)
By reading the code comments above the GlobalScope class, you can understand that the coroutines opened through CoroutineScope
are global, that is, they will not follow the life cycle of components (such as Activity), which may lead to some memory leaks.
So in order to solve this problem, Jetpack actually provides some extension components for Kotlin coroutines, such as LifecycleScope
and ViewModelScope
. The integration method is as follows:
dependencies {
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1'
}
Code language: JavaScript (javascript)
If it is used in a subclass of LifecycleOwner
(both AppCompatActivity
and Fragment
are its subclasses), the coroutine written in this way will be canceled when Lifecycle dispatches the destroy event
class TestActivity : AppCompatActivity() { fun test() { lifecycleScope.launch { } } }
If it is used in a subclass of ViewModel
, the coroutine written in this way will be canceled when the ViewModel
calls the clear method.
class TestViewModel : ViewModel() { fun test() { viewModelScope.launch() { } } }
What if I use coroutines outside of Lifecycle
or ViewModel
and worry about memory leaks?
val launch = GlobalScope.launch() { } launch.cancel()
You can manually call the cancel method at the right time, so you can cancel
println("Test start " + (Thread.currentThread() == Looper.getMainLooper().thread))
val job = GlobalScope.launch() {
try {
println("Test delay start " + (Thread.currentThread() == Looper.getMainLooper().thread))
delay(20000)
println("Test delay end")
delay(20000)
println("Test delay end")
} catch (e: CancellationException) {
println("Test canceled")
}
}
println("Test end")
job.cancel()
Code language: JavaScript (javascript)
Result:
12:35:10.005 System.out: Test start true
12:35:10.022 System.out: Test end
12:35:10.023 System.out: Test delay start false
12:35:10.027 System.out: Test canceled
Code language: CSS (css)