Kotlin Coroutines are pretty simple
Kotlin coroutine basic knowledge, a understand.
This article is available at github.com/mengdd/Kotl…
Coroutines concept
Coroutines, computer program components that support non-preemptive multitasking by allowing tasks to suspend and resume execution (see Wiki).
Coroutines are designed primarily for asynchronous, non-blocking code. This concept is not unique to Kotlin; it is supported in Go, Python, and many other languages.
Kotlin Coroutines
Kotlin uses coroutines for both asynchronous and non-blocking tasks, the main advantage being that the code is readable and no callbacks are needed. (Asynchronous code written with coroutines looks a lot like synchronous code at first glance.)
Kotlin’s support for coroutines is at the language level, providing minimal APIs in the standard library, and then proxying a lot of functionality into the library.
Only suspend is added as the keyword in Kotlin. Async and await are not Kotlin keywords and are not part of the standard library.
Kotlin’s notion of suspending function provides a safer and less fallible abstraction for asynchronous manipulation than futures and promises.
Kotlinx. coroutines is a library of coroutines, and in order to use its core functionality, a project needs to add a dependency on Kotlinx-Coroutines -core.
Coroutines Basics: What exactly are Coroutines?
Let’s start with an official demo:
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.delay import kotlinx.coroutines.launch fun main() { GlobalScope.launch { // launch a new coroutine in background and continue delay(1000L) // non-blocking delay for 1 second (default time unit is ms) println("World!" ) // print after delay } println("Hello,") // main thread continues while coroutine is delayed Thread.sleep(2000L) // block main thread for 2 seconds to keep JVM alive }Copy the code
The output of this code is Hello, and after 1s delay, World.
An explanation of this code:
Launch starts a computation, which is suspendable. It releases the underlying thread during the computation and resumes when the coroutine completes.
This kind of suspended computation is called a coroutine. So we can simply say launch a new coroutine.
Note that the main Thread waits for the coroutine to finish, and if you comment out thread.sleep (2000L) on the last line, you print Hello, not World.
Coroutines and threads
Coroutines can be understood as lightweight threads. Multiple coroutines can run in parallel, waiting and communicating with each other. The big difference between coroutines and threads is that coroutines are very cheap. We can create thousands of coroutines without worrying about performance.
A coroutine is an operation running on a thread that can be suspended. It can be suspended, meaning operations can be suspended, removed from the thread, and stored in memory. At this point, the thread is free to do other things. When the computation is ready to continue, it returns to the thread (but not necessarily to the same thread).
By default, coroutines run in a shared thread pool. Threads still exist, but one thread can run multiple coroutines, so there’s no need for too many threads.
debugging
Add the thread name to the code above:
fun main() { GlobalScope.launch { // launch a new coroutine in background and continue delay(1000L) // non-blocking delay for 1 second (default time unit is ms) println("World! + ${Thread.currentThread().name}") // print after delay } println("Hello, + ${Thread.currentThread().name}") // main thread continues while coroutine is delayed Thread.sleep(2000L) // block main thread for 2 seconds to keep JVM alive }Copy the code
You can Edit the IDE Configurations set in the VM options: – Dkotlinx. Coroutines. Debug, run the program, print out the code in the log run coroutines information:
Hello, + main
World! + DefaultDispatcher-worker-1 @coroutine#1
Copy the code
suspend function
The delay method in the above example is a suspend function.delay () and thread.sleep (). The delay() method can delay the coroutine without blocking the thread (It doesn’t block a thread, but only Suspends the Coroutine itself). Thread.sleep() blocks the current Thread.
So, suspend means that the coroutine scope is suspended, but code outside the coroutine scope in the current thread is not blocked.
If you replace GlobalScope.launch with thread, the delay method will display a red line error:
Suspend functions are only allowed to be called from a coroutine or another suspend function
Copy the code
The suspend method can only be called from a coroutine or from another suspend method.
During The coroutine wait, The thread will return to The thread pool. When The coroutine wait is finished, The coroutine will be returned to an idle thread in The thread pool. and when the waiting is done, the coroutine resumes on a free thread in the pool.)
Start the coroutines
To start a new coroutine, use the following methods:
launch
async
runBlocking
They are called Coroutine Builders. Different libraries can define many more ways to build.
RunBlocking: Connect the blocking and non-blocking worlds
RunBlocking is used to connect blocking and non-blocking worlds.
RunBlocking sets up a coroutine that blocks the current thread. So it’s mostly used in main or in tests, as a join function.
For example, the previous example could be rewritten as:
fun main() = runBlocking<Unit> {
// start main coroutine
GlobalScope.launch {
// launch a new coroutine in background and continue
delay(1000L)
println("World! + ${Thread.currentThread().name}")
}
println("Hello, + ${Thread.currentThread().name}") // main coroutine continues here immediately
delay(2000L) // delaying for 2 seconds to keep JVM alive
}
Copy the code
Finally, instead of thread.sleep (), use delay(). Program output:
Hello, + main @coroutine#1
World! + DefaultDispatcher-worker-1 @coroutine#2
Copy the code
Launch: return to the Job
The above example is not a good way to wait for a coroutine to end.
Launch returns Job, representing a coroutine, which we can explicitly wait for to end using Job’s join() method:
fun main() = runBlocking {
val job = GlobalScope.launch {
// launch a new coroutine and keep a reference to its Job
delay(1000L)
println("World! + ${Thread.currentThread().name}")
}
println("Hello, + ${Thread.currentThread().name}")
job.join() // wait until child coroutine completes
}
Copy the code
The output is the same as above.
Another important use of Job is cancel(), which cancellations coroutine tasks that are no longer needed.
Async: Returns a value from a coroutine
Async opens the coroutine and returns Deferred
, which is a subclass of Job and has an await() function that returns the result of the coroutine.
Await () is also a suspend function and can only be called inside a coroutine.
fun main() = runBlocking {
// @coroutine#1
println(Thread.currentThread().name)
val deferred: Deferred<Int> = async {
// @coroutine#2
loadData()
}
println("waiting..." + Thread.currentThread().name)
println(deferred.await()) // suspend @coroutine#1
}
suspend fun loadData(): Int {
println("loading..." + Thread.currentThread().name)
delay(1000L) // suspend @coroutine#2
println("loaded!" + Thread.currentThread().name)
return 42
}
Copy the code
Running results:
main @coroutine#1 waiting... main @coroutine#1 loading... main @coroutine#2 loaded! main @coroutine#2 42Copy the code
The Context, the Dispatcher and Scope
Take a look at the launch method declaration:
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job {
...
}
Copy the code
There are a couple of related concepts that we need to look at.
Coroutines are always run in a context of type CoroutineContext. The coroutine context is a collection of indexes that contain elements, including Job and dispatcher. The Job represents the coroutine, so what does the Dispatcher do?
Coroutine Builders: launch, Async, which build coroutines, are all extension methods of the CoroutineScope type. See the CoroutineScope interface, which contains a reference to CoroutineContext. What does it do?
Here’s how to answer those questions.
Dispatchers and thread
The CoroutineDispatcher in the Context can specify what thread the coroutine is running on. It can be a specified thread, thread pool, or unlimited.
Look at an example:
fun main() = runBlocking<Unit> { launch { // context of the parent, main runBlocking coroutine println("main runBlocking : I'm working in thread ${Thread.currentThread().name}") } launch(Dispatchers.Unconfined) { // not confined -- will work with main thread println("Unconfined : I'm working in thread ${Thread.currentThread().name}") } launch(Dispatchers.Default) { // will get dispatched to DefaultDispatcher println("Default : I'm working in thread ${Thread.currentThread().name}") } launch(newSingleThreadContext("MyOwnThread")) { // will get its own new thread println("newSingleThreadContext: I'm working in thread ${Thread.currentThread().name}") } }Copy the code
Print out after running:
Unconfined : I'm working in thread main
Default : I'm working in thread DefaultDispatcher-worker-1
newSingleThreadContext: I'm working in thread MyOwnThread
main runBlocking : I'm working in thread main
Copy the code
The API provides several options:
Dispatchers.Default
Represents the use of a shared thread pool on the JVM, whose size is determined by the number of CPU cores, but even a single core has two threads. Usually used for CPU intensive tasks such as sorting or complex calculations.Dispatchers.Main
Specifies the main thread used for UI update-related tasks (dependencies need to be added, for example)kotlinx-coroutines-android
.). If we start a new coroutine on the main thread and the main thread is busy, the coroutine will also be suspended and will resume execution only when the thread is free.Dispatchers.IO
: On-demand created thread pool for network or file reading and writing.Dispatchers.Unconfined
: does not specify a specific thread, this is a special dispatcher.
If dispatcher is not explicitly specified, the coroutine inherits the scope context(which contains dispatcher) from which it was launched.
In practice, the dispatcher of an external scope is preferred, with the context determined by the caller. It’s also easy to test.
NewSingleThreadContext creates a thread to run the coroutine, and a dedicated thread is an expensive resource that needs to be released or reused in real applications.
You can also switch threads with withContext, you can run code in the specified coroutine context, suspend it until it finishes, and return the result. Another approach is to start a new coroutine and explicitly suspend the wait with join.
In this UI Android applications, the more common approach is to coping with CoroutineDispatchers coroutines. Main, when you need to do something on other threads, and specify a different dispatcher.
What is a Scope?
When launch, async, or runBlocking opens a new coroutine, they automatically create the corresponding scope. All of these methods take a lambda argument with receiver. The default receiver type is CoroutineScope.
The IDE will prompt this: CoroutineScope:
launch { /* this: CoroutineScope */
}
Copy the code
When we create a new coroutine inside the curly braces of runBlocking, launch, or async, it is automatically created within this scope:
fun main() = runBlocking { /* this: CoroutineScope */ launch { /* ... */ } // the same as: this.launch { /* ... * /}}Copy the code
Since launch is an extension method, the default receiver in the above example is this. The coroutine launched by launch in this example is called the child of a runBlocking coroutine. This “parent-child” relationship is passed by scope: the child starts in the parent’s scope.
The father-son relationship of coroutines:
- When a coroutine is started in another coroutine’s scope, its context is automatically inherited, and the new coroutine’s Job is the child of the parent coroutine’s Job.
Therefore, there are two key points about scope:
- When we start a coroutine, we always start a coroutine
CoroutineScope
In the water. - Scope is used to manage parent-child relationships and structures between different coroutines.
The parent-child relationship of coroutines has the following two properties:
- When the parent coroutine is cancelled, all child coroutines are cancelled.
- The parent coroutine will always wait for all child coroutines to finish.
Note that it is also possible to create a new scope without starting the coroutine. A scope can be created using the factory method: MainScope() or CoroutineScope().
The coroutineScope() method can also create scopes. When we need to start a new coroutine inside the suspend function in a structured way, the new scope we create automatically becomes the child of the external scope from which the suspend function is called.
Therefore, the parent relationship can be further abstracted to the extent that there is no parent coroutine, and scope manages all child coroutines.
What problems does Scope solve in practical application? If an object in your application has a life cycle of its own, but that object is not a coroutine, for example, an Activity in an Android application that starts some coroutines to do asynchronous operations, update data, etc., and then cancels all coroutines when the Activity is destroyed, To avoid memory leaks. We can use CoroutineScope to do this by creating a CoroutineScope object bound to the activity’s lifecycle, or by having the activity implement the CoroutineScope interface.
Therefore, the main purpose of scope is to record all coroutines and cancel them.
A CoroutineScope keeps track of all your coroutines, and it can cancel all of the coroutines started in it.
Copy the code
Structured Concurrency
This structured concurrency mechanism using scope to structure coroutines is called “Structured concurrency.” The benefits are:
- Scope is automatically responsible for subcoroutines, whose lives and scope bindings.
- Scope automatically cancels all subcoroutines.
- Scope automatically waits for all subcoroutines to finish. If a scope is bound to a parent coroutine, the parent coroutine waits for all the children in the scope to complete.
With this structured concurrency mode, we can specify the main context once when creating a top-level coroutine, and all nested coroutines will automatically inherit the context and only modify it as needed.
GlobalScope: daemon
The coroutines GlobalScope launches are independent and their lives are limited only by the application. That is, the coroutine GlobalScope starts has no parent and no relation to the external scope in which it is started.
launch(Dispatchers.Default) { … } and Globalscope.launch {… } the dispatcher is the same.
The coroutine GlobalScope starts does not keep the process active. They are like Daemon Threads, and if the JVM finds that there are no other common threads, it shuts them down.
Key takeaways
- Coroutine mechanism: suspend, resume, simplify callback code.
- Suspend method.
- Several ways to start coroutines.
- Dispatcher specifies a thread.
- Structured Concurrency: To structure management coroutines based on a scope.
reference
- Coroutine Wiki
- Official document Overview page
- Official documentation Coroutines Guide
- Asynchronous Programming Techniques
- Your first coroutine with Kotlin
- Introduction to Coroutines and Channels
- Github: Kotlin/kotlinx.coroutines
- Github: Coroutines Guide
- Github: KEEP: Kotlin Coroutines
Third Party blog:
- Coroutines on Android (part I): Getting the background
- Async Operations with Kotlin Coroutines — Part 1
- Kotlin Coroutines Tutorial for Android
- Coroutine Context and Scope