Why
- Simplify writing asynchronous code.
- Enforcing strict mainline security ensures that your code never accidentally blocks the main thread, and improves code readability.
- Improve code security without memory leaks.
- Intercoroutine communication.
What
The concept of coroutines has been around since the early days of programming languages, with the first use of coroutines in Simula in 1967. Coroutines are like very lightweight threads. Threads are scheduled by the system, and the overhead of thread switching or thread blocking is high. Coroutines depend on threads, but do not block threads when they are suspended, and are virtually costless. The coroutines are controlled by the developer. So coroutines are also like user threads, very lightweight, you can create any coroutines in one thread.
But Kotlin’s coroutine isn’t really a coroutine, just another encapsulation that makes it easier to write asynchronous code as if it were synchronous. See: Are Kotlin coroutines really more efficient than Java threads?
Give me a chestnut that your girl will understand. If want from A subway station to the subway station C dating your sister, but when the B metro station, would you like to come to want to go to buy A nice present for his sister, it’s about an hour, but from A station to station C Only this one train, just drive fast, every 10 minutes were A standing start afresh again, the subway is like A thread, You go to buy gifts and get back to station B like a mission. In the case of synchronous blocking, the subway waits for you until you come back with your gift while you’re shopping for it. In a coroutine situation, you go to buy a gift like a coroutine, the subway drops you off at B, the subway keeps moving, you buy the gift and you wait at B for the next train, get on the train and go on a date with a girl. In the asynchronous case, you go to buy a gift (the asynchronous task) and the train continues on, but the subway driver gives you a phone number (callback). When you buy a gift and return to B, you need to call my phone number to let you get on the train. There is a problem with asynchronous callback. The driver has to give a phone number to each person who gets off the bus to do temporary errands. If he or she fails to come back, the phone number of the driver may be leaked, which is very unsafe.
How
Common problems encountered in Android development:
- Long running tasks
- Main-safety
- Leak work
Long running tasks
- One CPU cycle is less than 0.0000000004 seconds
- A network request takes about 0.4 seconds
In Android, the main thread is mainly user UI rendering and responding to user gesture interaction, as well as lightweight logic operations. If a request is made on the main thread, the application will slow down, get stuck, and be unable to respond to user interactions, easily resulting in ARN and poor user experience. So the common practice in the industry is to implement asynchronous callbacks through callback:
class ViewModel: ViewModel() {
fun fetchDocs() {
get("developer.android.com") { result ->
show(result)
}
}
}
Copy the code
The above example of callback is just a case of one level of callback. If there are two or more asynchronous requests, and the next request depends on the result of the previous request, there can be layers of nesting. Of course, the current popular approach is to use Retrofit’s transformation function flatMap to implement chained calls, but the code still looks bloated. The code above can be simplified like this if coroutines are used:
// Dispatchers.Main
suspend fun fetchDocs() {
// Dispatchers.Main
val result = get("developer.android.com")
// Dispatchers.Main
show(result)
}
suspend fun get(url: String) = withContext(Dispatchers.IO) {
// Make a request
// Dispatchers.IO
}
Copy the code
Coroutines provides a great way to simplify the coding of time-consuming tasks by allowing asynchronous callback code to be written sequentially as synchronous code. Coroutines adds two new operations to the normal method. In addition to call and return, Coroutines also adds suspend and resume. Coroutines use stack frames to manage the currently running method and all of its local variables. When the coroutine starts to hang, the current stack frame is copied and saved for later use. When the coroutine begins to be restored, the stack frame is restored from where it was saved, and the current stack frame method continues.
- Suspend: Suspends execution of the current coroutine and copies and saves all local variables and functions of the current execution stack frame.
- Resume: Resumes the execution of the current coroutine from where it was suspended.
Suspend Functions can only be called from coroutines or suspend functions.
Main-safety with coroutines
In Kotlin coroutines, well written suspend functions should always be safely called from the main thread and should be allowed to be called from any thread. Using the suspend decorated function does not tell Kotlin that the method is running on the main thread.
To write a mainline-safe time-consuming method, you can have coroutines executed in Default or IO Dispatcher (with withContext(dispatchers.io) specified to run in IO threads). In coroutines all coroutines must run in the Dispatcher, even if they are running in the main thread. Coroutines will hang themselves and dispatcher knows how to recover them.
To specify what thread coroutines are running on, Kotlin provides four types of Dispatchers:
Dispatchers | use | Usage scenarios |
---|---|---|
Dispatchers.Main | Main thread, interacting with the UI, performing light tasks | 1.call suspend functions . 2. callUI functions . 3.Update LiveData |
Dispatchers.IO | Used for network requests and file access | 1. Database . 2.Reading/writing files . 3.Networking |
Dispatchers.Default | CPU intensive task | 1. Sorting a list . 2.Parsing JSON . 3.DiffUtils |
Dispatchers.Unconfined | There is no restriction on any formulation thread | Advanced scheduler, should not be used in regular code |
If you use suspend Functions, RxJava, and LiveData in Room, it automatically provides mainline security.
// Dispatchers.Main
suspend fun fetchDocs() {
// Dispatchers.Main
val result = get("developer.android.com")
// Dispatchers.Main
show(result)
}
// Dispatchers.Main
suspend fun get(url: String) =
// Dispatchers.IO
withContext(Dispatchers.IO) {
// Dispatchers.IO
/* perform blocking network IO here */
}
// Dispatchers.Main
Copy the code
Leak work
You may have thousands of coroutines in your program, and it’s hard to keep track of them manually in your code. If you keep track of them manually to make sure they’re done or canceled, your code will be bloated and error-prone. If the code is not perfect, it can lose track of the Coroutine and cause the task to leak. Task leaks are like memory leaks, but worse. It not only wastes memory usage, but also CPU, disk, and even makes a network request.
In Android, we know that activities and fragments have a life cycle, and our usual pattern is to cancel all asynchronous tasks when the current page exits. If there is an asynchronous network request that is still being executed at the time of the current page destruction, what problems can result:
- Null pointer exception. An unexpected null pointer exception was caused when the UI state was updated for the result of the request.
- Waste of memory resources.
- Waste of CPU resources.
To avoid coroutine leakage, Kotlin introduced structured concurrency. Structured concurrency is a combination of language features and best practices that, if followed, help track tasks running in coroutines. Structuring concurrency in Android helps us do three things:
- Cancels the task when the coroutine is no longer needed.
- Trace the task while the coroutine is running.
- Propagate error signals when coroutine execution fails.
Solution:
- The CoroutineScope cancels a task through the associated job.
- Task tracking, coroutines structured concurrency through
coroutineScope
和supervisorScope
ensuresuspend function
Do not return until all tasks are completed. - Propagation of error signal.
coroutineScope
Error is guaranteed to pass in both directions. Whenever a child coroutine fails or an exception occurs, the exception is passed to the parent field and all child Coroutines are cancelled. whilesupervisorScope
Implement one-way error passing for job monitors.
CoroutineScope
In Kotlin all coroutines must run on the CoroutineScope. Scope keeps track of the status of all coroutines, but unlike the Dispatcher, it does not run your coroutines. It cancels all coroutines started inside. Start a new coroutine:
scope.launch {
// This block starts a new coroutine
// "in" the scope.
//
// It can call suspend functions
fetchDocs()
}
Copy the code
createCoroutineScope
The common ways are as follows:
CoroutineScope(context: CoroutineContext)
, API methods, such as:val scope = CoroutineScope(Dispatchers.Main + Job())
Or as follows:
class LifecycleCoroutineScope : CoroutineScope, Closeable {
private val job = JobSupervisorJob()
override val coroutineContext: CoroutineContext
get() = Dispatchers.Main + job
override fun close() {
job.cancel()
}
}
Copy the code
class SimpleRetrofitActivity : FragmentActivity() {
private val activityScope = LifecycleCoroutineScope()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_simple_retrofit)
// some other code ...
}
override fun onDestroy() {
super.onDestroy()
activityScope.close()
}
// some other code ...
}
Copy the code
coroutineScope
: API method that creates a new subdomain and manages all coroutines in the domain. Note that this method does not exit until all of the subcoroutines created in the block have been executed.supervisorScope
And:coroutineScope
The difference is that when a child coroutine fails, the error is not passed up to the parent field, so it does not affect the child coroutine.
A common way to create coroutines is as follows:
lauch
: coroutine builder, which creates and starts (or delays) a coroutine and returns a Job, which is used to monitor and cancel tasks, for scenarios with no return value.async
Coroutine builder, just like Launch, except that it returns a subclass of JobDeferred
The only difference is that the return value of completion can be obtained with await, or an exception can be caught (exception handling is different).
In Android there is an extension library for Kotlin’s ViewModel lifecycle- ViewModel-ktx :2.1.0-alpha04, which can be started with the viewModelScope extension property, The viewModelScope binds to the activity’s life cycle, and automatically cancels all coroutines started in the scope when the activity is destroyed.
fun runForever() {
// start a new coroutine in the ViewModel
viewModelScope.launch {
// cancelled when the ViewModel is cleared
while(true) {
delay(1_000)
// do something every second
}
}
}
Copy the code
Note that cancellation of coroutines is cooperative. Cancelling while a coroutine is suspended will throw a CancellationException. Even if you catch the exception, the state of the coroutine changes to canceled. If you are a computational coroutine and do not check for cancellation status, the coroutine cannot be canceled.
val startTime = System.currentTimeMillis() val job = launch(Dispatchers.Default) { var nextPrintTime = startTime var i = 0while (i < 5) { // computation loop, just wastes CPU
// print a message twice a second
if (System.currentTimeMillis() >= nextPrintTime) {
println("job: I'm sleeping ${i++} ...")
nextPrintTime += 500L
}
}
}
delay(1300L) // delay a bit
println("main: I'm tired of waiting!")
job.cancelAndJoin() // cancels the job and waits for its completion
println("main: Now I can quit.")
Copy the code
Task tracking
Sometimes, we want two or more requests to run concurrently and wait for them all to complete. The suspend Function and the subfields created by coroutineScope guarantee that all the subcoroutines are complete before returning.
suspend fun fetchTwoDocs() {
coroutineScope {
launch { fetchDoc(1) }
async { fetchDoc(2) }
}
}
Copy the code
Propagation of error signal
Note that the structured concurrency of coroutines is based on language features plus best practices, and errors are lost as follows:
val unrelatedScope = MainScope()
// example of a lost error
suspend fun lostError() {
// async without structured concurrency
unrelatedScope.async {
throw InAsyncNoOneCanHearYou("except")}}Copy the code
The above code is missing because the async recovery requires an await call so that the exception can be re-uploaded, and another coroutine field is used in the suspend Function, causing lostError to exit without waiting for the job to complete. Properly structured concurrency:
suspend fun foundError() {
coroutineScope {
async {
throw StructuredConcurrencyWill("throw")}}}Copy the code
You can create unstructured coroutines via CoroutineScope (note the capital C) and GlobalScope, only if you think it has a longer lifetime than the caller.
conclusion
CoroutineScope
: Coroutine scope containsCoroutineContext
To start coroutines and trace child coroutines, which are actually traced by Job.CoroutineContext
: Coroutine context, mainly containingJob
andCoroutineDispatcher
Represents the scenario of a coroutine.CoroutineDispatcher
: Coroutine scheduler that determines the thread or thread pool in which the coroutine resides. It can specify that the coroutine runs on a particular thread, a thread pool, or no thread at all.Job
: task, which encapsulates the code logic that needs to be executed in the coroutine. A Job can be cancelled and has a simple life cycle, which has three states:isActive
,isCompleted
,isCancelled
.Deferred
: Subclass of Job, Job with return value, passesawait
To obtain.- Coroutine builders include:
lauch
,async
.
The sample
- Github.com/hexi/Kotlin…
reference
- Coroutines – guide (website)
- kotlinx.coroutines(github)
- Improve app performance with Kotlin coroutines
- Coroutines on Android (part I): Getting the background
- Coroutines on Android (part II): Getting started
- Coroutines On Android (part III): Real work
- Are Kotlin coroutines really more efficient than Java threads?