The official document: kotlinlang.org/docs/corout… Github address: github.com/Kotlin/kotl… Coroutines example: play.kotlinlang.org/hands-on/In…
A list,
Coroutines are a concurrent design pattern that you can use on the Android platform to simplify code that executes asynchronously. A coroutine is a piece of code that can be suspended, and can be thought of as a lightweight thread. The relation between coroutines and threads: (1) Any coroutine can be created in a thread; (2) Execution, suspension and recovery of coroutines depend on threads, but there is no need to block threads when coroutines are suspended; (3) The coroutine need not specify a thread, that is, the coroutine can be suspended in one thread and then resumed in another thread;
1.1 Principle of coroutine suspension recovery
Each suspend decorated method or lambda expression adds an additional continuation-type argument to it when the code is called, and returns Any? Type.
/** * Interface representing a continuation after a suspension point that returns a value of type `T`. */
@SinceKotlin("1.3")
public interface Continuation<in T> {
/** * The context of the coroutine that corresponds to this continuation. */
public val context: CoroutineContext
/** * Resumes the execution of the corresponding coroutine passing a successful or failed [result] as the * return value of the last suspension point. */
public fun resumeWith(result: Result<T>)
}
Copy the code
When the suspend function is suspended by a coroutine, it returns a special identifier, COROUTINE_SUSPENDED, which is essentially an Any; When the coroutine executes without suspension, it returns the result of execution or the exception thrown. So in order to support returns in both cases, we use Kotlin’s unique Any, right? Type.
1.2 Coroutine lightweight principle
(1) Small footprint Each thread has its own stack, usually 1MB. The minimum stack space allowed per thread in the JVM is 64K, while a simple coroutine in Kotlin occupies only a few tens of bytes of heap memory. But coroutine dependent Dispatchers have a limit on the number of threads. The coroutine is managed as suspend and resume by a callback-like object Continuation, which is added as the last argument to the function marked suspend keyword at compile time. The function, like any other object, is in the heap and is used to recover the coroutine. You don’t need thousands of MB of space in RAM to keep all the threads active. A typical 60-70 thread is created at Max using CommonPool and is reused (if a new coroutine is created, it will wait for another thread to complete).
2. Basic Usage
2.1 Adding a Dependency
First, add a dependency on the Kotlin plug-in in the project build.gradle
buildscript {
ext.kotlin_version = '1.5.0
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"}}Copy the code
Then rely on the Kotlin library and Kotlin Coroutine in the Module:
apply plugin: 'kotlin-android'
dependencies {
// The kotlin library
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
// kotlin coroutine
implementation 'org. Jetbrains. Kotlinx: kotlinx coroutines - android: 1.5.0 - RC'
}
Copy the code
2.2 Simple Example
// Launch is the builder of a coroutine. It launches a coroutine that can run independently
GlobalScope.launch {
// Suspend the current coroutine; Delay is a suspend function :public suspend fun delay(timeMillis: Long); The suspension of coroutines does not block the current thread;
delay(1000)
Log.d("CoroutineTag"."Kotlin Coroutines World!")
}
Log.d("CoroutineTag"."hello")
Copy the code
The running results are as follows:
>hello
>Kotlin Coroutines World!
Copy the code
2.3 GlobalScope.launch source code analysis
In the 2.2 code, GlobalScope is an instance of CoroutineScope; Launch is the CoroutineScope extension function used to launch coroutines. The extension function is defined as follows:
/** * start a new coroutine and return the coroutine as Job; * The execution of the coroutine can be controlled by Job start(), cancle(), join() and other methods; * * Receives three arguments *@paramContext Coroutine context *@paramStart Specifies the startup mode of the coroutine. The DEFAULT value is CoroutineStart.DEFAULT *@paramBlock requires the suspended function * to be called@returnJob returns the newly created coroutine **/ as job
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)
// The suspension function passed in the argument is executed here
coroutine.start(start, coroutine, block)
return coroutine
}
Copy the code
Coroutine mechanism CoroutineScope, Coroutinescope.launch (), CoroutineContext, CoroutineStart
Third, CoroutineScope
Coroutine Scope is the Scope of Coroutine operation. Each asynchronous operation runs within a specific scope. Its definition is as follows:
/** * CoroutineScope defines CoroutineScope; * When a new CoroutineScope is created and a new coroutine is launched via coroutinescope.launch or coroutinescope.ayNC, the new CoroutineScope inherits the external CoroutineScope's Coroutin as well eContext; * /
public interface CoroutineScope {
public val coroutineContext: CoroutineContext
}
Copy the code
3.1 create CoroutineScope
Use the appropriate CoroutineScope and destroy it at the appropriate time to avoid memory leaks. For example, cancel coroutines in the CoroutineScope during Activity destruction to avoid memory leaks. CoroutineScope(1) CoroutineScope(); Generic CoroutineScope(); (2) MainScope(); The scope is UI life cycle, and the default scheduler is dispatchers.main; As follows:
class MyAndroidActivity {
private val scope = MainScope()
override fun onDestroy(a) {
super.onDestroy()
scope.cancel()
}
}
Copy the code
The following describes the common CoroutineScope and the scenarios in which a CoroutineScope is created:
3.2 GlobalScope
Coroutines in GlobalScope scope can be executed after an App is started until the execution of the coroutine ends or is cancelled. They are often used to launch top-level coroutines that need to run during the application lifecycle and cannot be cancelled early. It is not recommended to use GlobalScope to start coroutines that no longer need to be executed after an Activity or Fragment has been destroyed.
3.3 CoroutineScope. Launch
GlobalScope.launch {
Log.d("CoroutineTag"."GlobalScope")
launch {
// This in coroutine is CoroutineScope
Log.d("CoroutineTag"."coroutineScope")}}Copy the code
Launch creates a CoroutineScope that inherits the CoroutineContext of the external CoroutineScope.
3.4 CoroutineScope.aync
GlobalScope.launch {
Log.d("CoroutineTag"."GlobalScope")
async {
// This in coroutine is CoroutineScope
Log.d("CoroutineTag"."coroutineScope")}}Copy the code
With CoroutineScope. Async, a CoroutineScope is created that inherits the CoroutineContext of the external CoroutineScope. The difference between coroutinescope.async and Coroutinescope.launch is that async returns Deferred and can retrieve the execution result of the coroutine.
3.5 coroutineScope
Within other coroutines or suspended functions, a coroutineScope can be created using coroutineScope, and the newly created coroutine in coroutineScope{} will be executed immediately, as shown below:
GlobalScope.launch {
Log.d("CoroutineTag"."GlobalScope")
coroutineScope {
// This in coroutine is CoroutineScope
Log.d("CoroutineTag"."coroutineScope")}}Copy the code
CoroutineScope is a suspend suspend function, so it can only be called inside a coroutine or suspend function:
public suspend fun <R> coroutineScope(block: suspend CoroutineScope. () - >R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return suspendCoroutineUninterceptedOrReturn { uCont ->
val coroutine = ScopeCoroutine(uCont.context, uCont)
coroutine.startUndispatchedOrReturn(coroutine, block)
}
}
Copy the code
3.6 runBlocking
In a thread, coroutine, or suspended function, you can create a CoroutineScope with runBlocking and immediately execute the new coroutine in runBlocking{} as shown below:
GlobalScope.launch {
Log.d("CoroutineTag"."GlobalScope")
runBlocking {
// This in coroutine is CoroutineScope
Log.d("CoroutineTag"."coroutineScope")}}Copy the code
Unlike coroutineScope, runBlocking blocks the current thread; RunBlocking is a normal method, so runBlocking can be called in a thread:
/ * * *@paramBlock suspends the function and returns T, which is the result of the coroutine */
public fun <T> runBlocking(context: CoroutineContext = EmptyCoroutineContext, block: suspend CoroutineScope. () - >T): T {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
val currentThread = Thread.currentThread()
val contextInterceptor = context[ContinuationInterceptor]
val eventLoop: EventLoop?
val newContext: CoroutineContext
if (contextInterceptor == null) {
// create or use private event loop if no dispatcher is specified
eventLoop = ThreadLocalEventLoop.eventLoop
newContext = GlobalScope.newCoroutineContext(context + eventLoop)
} else {
// See if context's interceptor is an event loop that we shall use (to support TestContext)
// or take an existing thread-local event loop if present to avoid blocking it (but don't create one)
eventLoop = (contextInterceptor as? EventLoop)? .takeIf { it.shouldBeProcessedFromContext() } ? : ThreadLocalEventLoop.currentOrNull() newContext = GlobalScope.newCoroutineContext(context) }val coroutine = BlockingCoroutine<T>(newContext, currentThread, eventLoop)
coroutine.start(CoroutineStart.DEFAULT, coroutine, block)
return coroutine.joinBlocking()
}
Copy the code
3.7 MVVM的CoroutineScope
Reference: developer.android.com/topic/libra…
For MVVM architecture, KTX provides LifecycleOwner, LiveData, ViewModel corresponding CoroutineScope; The usage method is as follows: (1) Add dependency
dependencies {
// ViewModelScope
implementation "Androidx. Lifecycle: lifecycle - viewmodel - KTX: 2.2.0." "
// LifecycleScope
implementation "Androidx. Lifecycle: lifecycle - runtime - KTX: 2.2.0." "
// liveData
implementation "Androidx. Lifecycle: lifecycle - livedata - KTX: 2.2.0." "
}
Copy the code
(2) Usage examples
// 1.viewModelScope
class MyViewModel: ViewModel() {
init {
viewModelScope.launch {
// Coroutine that will be canceled when the ViewModel is cleared.}}}// 2.lifecycleScope
class MyFragment: Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?). {
super.onViewCreated(view, savedInstanceState)
viewLifecycleOwner.lifecycleScope.launch {
val params = TextViewCompat.getTextMetricsParams(textView)
val precomputedText = withContext(Dispatchers.Default) {
PrecomputedTextCompat.create(longTextContent, params)
}
TextViewCompat.setPrecomputedText(textView, precomputedText)
}
}
}
Copy the code
4. Coroutine startup mode
Coroutine Builder coroutines can be started in several ways:
4.1 CoroutineScope. Launch
Start a coroutine that does not block the calling thread and must be called in the CoroutineScope CoroutineScope. Return Job to indicate the newly created coroutine.
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
4.2 CoroutineScope. Async
A coroutine that starts but does not block the calling thread must be called in the CoroutineScope CoroutineScope. Returning Deferred indicates the newly created coroutine. Deferred is a subinterface of the Job.
public fun <T> CoroutineScope.async(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope. () - >T
): Deferred<T> {
val newContext = newCoroutineContext(context)
val coroutine = if (start.isLazy)
LazyDeferredCoroutine(newContext, block) else
DeferredCoroutine<T>(newContext, active = true)
coroutine.start(start, coroutine, block)
return coroutine
}
Copy the code
The difference between coroutinescope.async and Coroutinescope.launch is that async returns Deferred and can retrieve the execution result of the coroutine.
4.3 withContext
Specify a CoroutineContext and start a coroutine as follows:
GlobalScope.launch {
withContext(Dispatchers.Default) {
}
Copy the code
WithContext is a suspended function that can only be called inside other coroutines or suspended functions:
public suspend fun <T> withContext(
context: CoroutineContext,
block: suspend CoroutineScope. () - >T
): T {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return suspendCoroutineUninterceptedOrReturn sc@ { uCont ->
// compute new context
val oldContext = uCont.context
val newContext = oldContext + context
// always check for cancellation of new context
newContext.checkCompletion()
// FAST PATH #1 -- new context is the same as the old one
if (newContext === oldContext) {
val coroutine = ScopeCoroutine(newContext, uCont)
return@sc coroutine.startUndispatchedOrReturn(coroutine, block)
}
// FAST PATH #2 -- the new dispatcher is the same as the old one (something else changed)
// `equals` is used by design (see equals implementation is wrapper context like ExecutorCoroutineDispatcher)
if (newContext[ContinuationInterceptor] == oldContext[ContinuationInterceptor]) {
val coroutine = UndispatchedCoroutine(newContext, uCont)
// There are changes in the context, so this thread needs to be updated
withCoroutineContext(newContext, null) {
return@sc coroutine.startUndispatchedOrReturn(coroutine, block)
}
}
// SLOW PATH -- use new dispatcher
val coroutine = DispatchedCoroutine(newContext, uCont)
coroutine.initParentJob()
block.startCoroutineCancellable(coroutine, coroutine)
coroutine.getResult()
}
}
Copy the code
4.4 coroutineScope {}
As shown in 3.2, coroutineScope{} creates a coroutineScope{} and executes the newly created coroutine in {} without blocking the current thread.
4.5 runBlocking {}
Starting a new coroutine and blocking the calling thread returns the result of the suspended function as the result of the coroutine, which is commonly used for main methods and tests (so not commonly used in Android development).
4.6 produce < > {}
Start a new coroutine and produce a set of data to a channel. This coroutine returns a ReceiveChannel from which other coroutines can receive data.
Fifth, CoroutineContext
5.1 define
CoroutineContext is a collection of elements. The main elements are the Job, which represents the coroutine, and the Dispatcher, which represents the coroutine (Jobs, Dispatchers, and CoroutineName all implement the Element interface). CoroutineScope encapsulates CoroutineContext:
public interface CoroutineScope {
public val coroutineContext: CoroutineContext
}
Copy the code
5.2 inheritance
When a new coroutine is launched via coroutinescope.launch, the new CoroutineScope inherits the external CoroutineContext, and the new coroutine Job becomes a child Job of the parent coroutine, so that the child Job is recursively cancelled when the parent Job is cancelled.
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
Job will not be cancelled unless: (1) GlobalScope. Launch coroutine; (2) A specific parent Job is passed in the launch;
5.3 combination CoroutineContext
In some cases, we need to customize the elements in CoroutineContext. We can use + to combine the elements in CoroutineContext:
launch(Dispatchers.Default + CoroutineName("test")) {
println("I'm working in thread ${Thread.currentThread().name}")}Copy the code
Six, CoroutineStart
6.1 CoroutineStart. DEFAULT
Coroutinescope.launch () The DEFAULT startup mode for creating coroutines is coroutinestart.default, which is executed immediately.
6.2 CoroutineStart. LAZY
Coroutines with CoroutineStart.LAZY are not executed immediately and require a manual call to start(). You can set the startup mode to CoroutineStart.LAZY as follows:
val job = launch(start = CoroutineStart.LAZY) {
// ...
}
// Call start manually
job.start()
Copy the code
Seven, the Job
The coroutinescope. launch function returns a Job object that represents an asynchronous task. The execution of the coroutine can be controlled by Job start(), cancle(), join() and other methods.
7.1 Simple Example
val startTime = System.currentTimeMillis()
val job = launch(Dispatchers.Default) {
var nextPrintTime = startTime
var i = 0
while (isActive) { // cancellable computation loop
// 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."Shell Hello World! DoneCopy the code
“Done” is displayed only after the job is executed.
7.2 to get the Job
A CoroutineContext has only one Job. You can obtain the Job in the CoroutineContext in the following way:
println("My job is ${coroutineContext[Job]}")
Copy the code
The output is:
My job is "coroutine#1":BlockingCoroutine{Active}@6d311334
Copy the code
7.3 the Job status
Jobs have a life cycle and can be cancelled. A Job can also have hierarchical relationships. A Job can contain multiple child jobs. When the parent Job is cancelled, all child jobs are automatically cancelled. When the child Job is cancelled or an exception occurs, the parent Job is also cancelled.
/** ** A background job. ** ### job instances * The most basic instances of 'job' interface are created like this: * (1)Coroutine job is created with [launch][CoroutineScope.launch] coroutine builder. * It runs a specified block of code and completes on completion of this block. * (2)CompletableJob is created with a `Job()` factory function. * It is Completed by calling [CompletableJob.complete]. * * ### Job States * A Job has the following states: * * | **State** | [isActive] | [isCompleted] | [isCancelled] | * | -------------------------------- | ---------- | ------------- | ------------- | * | _New_ (optional initial state) | `false` | `false` | `false` | * | _Active_ (default initial state) | `true` | `false` | `false` | * | _Completing_ (transient state) | `true` | `false` | `false` | * | _Cancelling_ (transient state) | `false` | `false` | `true` | * | _Cancelled_ (final state) | `false` | `true` | `true` | * | _Completed_ (final state) | `false` | `true` | `false` | * */
Copy the code
Suspend functions
The suspend function is called a suspend function and can only be called inside a coroutine or another suspend function, as shown below;
suspend fun doSomethingUsefulOne(a): Int {
delay(1000L) // pretend we are doing something useful here
return 13
}
suspend fun doSomethingUsefulTwo(a): Int {
delay(1000L) // pretend we are doing something useful here, too
return 29
}
Copy the code
8.1 Sequential Execution
Suspend functions are executed sequentially, as follows:
val time = measureTimeMillis {
val one = doSomethingUsefulOne()
val two = doSomethingUsefulTwo()
println("The answer is ${one + two}")
}
println("Completed in $time ms")
Copy the code
The results are as follows:
The answer is 42
Completed in 2017 ms
Copy the code
8.2 Asynchronous Execution
Sync is used to asynchronously execute the suspend function as follows:
val time = measureTimeMillis {
val one = async { doSomethingUsefulOne() }
val two = async { doSomethingUsefulTwo() }
println("The answer is ${one.await() + two.await()}")
}
println("Completed in $time ms")
Copy the code
The results are as follows:
The answer is 42
Completed in 1017 ms
Copy the code
8.3 Delay asynchronous execution
val time = measureTimeMillis {
val one = async(start = CoroutineStart.LAZY) { doSomethingUsefulOne() }
val two = async(start = CoroutineStart.LAZY) { doSomethingUsefulTwo() }
// some computation
one.start() // start the first one
two.start() // start the second one
println("The answer is ${one.await() + two.await()}")
}
println("Completed in $time ms")
Copy the code
The results are as follows:
The answer is 42
Completed in 1017 ms
Copy the code
Coroutine scheduling
Coroutine uses the CoroutineDispatcher to schedule which thread the Coroutine executes on; CoroutineDispatcher implements the CoroutineContext interface, which can be used as follows:
GlobalScope.launch {
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}")}// Other ways of starting coroutines can also specify Dispatcher
async(Dispatchers.Default) {
}
withContext(Dispatchers.Default) {
}
}
Copy the code
The running results are as follows:
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
Here are some common patterns of the CoroutineDispatcher:
1. Dispatchers.Default:
The default value when launching a coroutine, using thread pools internally; Dispatchers.Default limits the number of cores on the processor (2, 4, 6, 8, etc.);
2. Dispatchers.Main
Execute coroutines in the main thread
3. Dispatchers.IO:
Performs coroutines on IO threads, typically for network or I/O operations, and shares the thread pool with dispatchers.default; Dispatchers.IO limits up to 64 threads;
4. Dispatchers.Unconfined
Dispatchers.Unconfined cannot be used to create new threads. Coroutines executed and recovered using this method are executed on the current thread.
Data communication between coroutines
(1) The Deferred returned by coroutinescope.async represents the execution result of the coroutine, and the transmission of a single value between coroutines can use this method; (2) Channels are used to pass a series of values between coroutines.
10.1 channels
Channels are similar to BlockingQueue, except that BlockingQueue.put() and Blockingqueue.take block, while channel.send() and channel.receive do not.
val channel = Channel<Int>()
launch {
// this might be heavy CPU-consuming computation or async logic, we'll just send five squares
for (x in 1.. 5) channel.send(x * x)
}
// here we print five received integers:
repeat(5) { println(channel.receive()) }
println("Done!")
Copy the code
The running results are as follows:
1
4
9
16
25
Done!
Copy the code