kotlin_ Synergetic process_ concept

Posted by thread_PHP on Sun, 19 Dec 2021 11:09:48 +0100

Reprinted from: https://www.jianshu.com/p/b8a3a42eea4c

kotlin coprocess concept

1, Kotlin synergy concept

Kotlin coroutine provides a new way to deal with concurrency. You can use it on Android platform to simplify asynchronous code execution. Coprocessing was introduced from kotlin version 1.3, but this concept existed at the dawn of the birth of the programming world. The earliest programming language using coprocessing can be traced back to the Simula language in 1967. In the past few years, the concept of collaborative process has developed rapidly and has been adopted by many mainstream programming languages, such as Javascript, C#, Python, Ruby and Go. Kotlin synergy is based on established concepts from other languages
Goggle officially recommends Kotlin collaboration as a solution for asynchronous programming on Android. The noteworthy function points include:

  • Lightweight: you can run multiple coroutines on a single thread, because the coroutine supports suspension and will not block the thread running the coroutine. Suspending saves memory than blocking and supports multiple parallel operations
  • Less memory leakage: use structured concurrency to perform multiple operations within a scope
  • Built in cancellation support: the cancellation function will be automatically propagated through the running collaboration hierarchy
  • Jetpack integration: many jetpack libraries contain extensions that provide comprehensive collaboration support. Some libraries also provide their own collaboration scope for you to use for structured concurrency

Import dependency:

    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutine_version"
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutine_version"

2, Synergetic training

A coroutine can be called a lightweight thread. Kotlin coprocesses are declared and started in the context of coroutine scope through coroutine builders such as launch and async

fun main() {
    GlobalScope.launch(context = Dispatchers.IO) {
        //One second delay
        delay(1000)
        log("launch")
    }
    //Active sleep for two seconds to prevent the JVM from exiting too quickly
    Thread.sleep(2000)
    log("end")
}

private fun log(msg: Any?) = println("[${Thread.currentThread().name}] $msg")


[DefaultDispatcher-worker-1 @coroutine#1] launch
[main] end

In the above example, Through GlobalScope (i.e. global scope) starts a process and outputs a log line after a delay of one second. From the output results, it can be seen that the started process runs in the Thread pool inside the process. Although from the performance results, starting a process is similar to using threads directly to execute time-consuming tasks, in fact, there are essential differences between processes and threads. Through Using CO process can greatly improve the concurrency efficiency of threads, avoid the previous nested callback hell, and greatly improve the readability of the code

The above code involves four basic concepts of collaborative process:

  • suspend function. That is, the suspend function. The delay function is a suspend function provided by the coroutine library to realize non blocking delay
  • CoroutineScope. That is, the scope of collaboration. GlobalScope is an implementation class of coroutinescope, which is used to specify the scope of collaboration and manage the life cycle of multiple collaboration processes. All collaboration processes need to be started through coroutinescope
  • CoroutineContext. That is, the collaboration context, which contains many types of configuration parameters. Dispatchers.IO is an implementation of the abstract concept of coroutinecontext, which is used to specify the running carrier of the collaboration, that is, to specify the thread on which the collaboration is to run
  • CoroutineBuilder. That is, the collaboration builder. The collaboration is declared and started in the context of CoroutineScope through the collaboration builders such as launch and async. Launch, async, etc. are declared as extension methods of CoroutineScope

3, suspend function

If the above example attempts to call the delay() function directly outside GlobalScope, the IDE will prompt an error: Suspend function 'delay' should be called only from a coroutine or another suspend function. The delay () function is a pending function that can only be called by a coroutine or other pending functions

The delay() function is decorated with suspend, and the function decorated with suspend is the suspend function

public suspend fun delay(timeMillis: Long)

When readers read articles about collaboration on the Internet, they should often see such a sentence: a suspended function will not block its thread, but will suspend the collaboration and resume it at a specific time

My understanding of this sentence is: the delay() function is similar to the thread in Java Sleep (), and the reason why the delay() function is non blocking is that it is essentially different from simple thread sleep. A coroutine runs on a thread, A thread can run multiple threads (tens of thousands of) coroutines. The scheduling behavior of threads is managed by the operating system, and the scheduling behavior of coroutines can be specified by developers and implemented by compilers. Coroutines can finely control the execution timing and threads of multiple tasks. When all coroutines on a particular thread are suspend ed, the thread can free up resources to deal with other tasks Affairs

For example, when CoroutineA running on ThreadA calls the delay(1000L) function to specify a delay of one second before running, ThreadA will turn to CoroutineB and wait until one second before continuing to execute CoroutineA. Therefore, thread a will not be blocked due to the delay of CoroutineA, but can continue to perform other tasks. Therefore, the hanging function will not block its thread, which greatly improves the concurrency flexibility of the thread and maximizes the utilization efficiency of the thread. And if you use thread Sleep (), the thread can only wait and cannot perform other tasks, which reduces the utilization efficiency of the thread

4, Suspend and resume of suspend function

Coroutine adds two operations on the basis of conventional functions to deal with long-running tasks. In addition to invoke (or call) and return, suspend and resume are added to the coroutine:

  • Suspend is used to suspend the execution of the current coroutine and save all local variables
  • Resume is used to resume the execution of the suspended process from the pause

The suspend function can only be called by other suspend functions or by a coroutine

The following example shows a simple co process implementation of a task (assuming that the get method is a network request task):

suspend fun fetchDocs() {                             // Dispatchers.Main
    val result = get("https://developer.android.com") // Dispatchers.IO for `get`
    show(result)                                      // Dispatchers.Main
}
suspend fun get(url: String) = withContext(Dispatchers.IO) { /* ... */ }

In the above example, get() is still called on the main thread, but it pauses the coroutine before starting the network request. In the get() body, a code block running in the IO thread pool is created by calling withContext(Dispatchers.IO). Any code in the block is always executed by the IO scheduler. When the network request is completed, get() will resume the suspended workflow, so that the main thread workflow can directly get the network request result without using a callback to notify the main thread. Retrofit supports collaborative processes in this way

Kotlin uses stack frames to manage which function to run and all local variables. When you pause a coroutine, the system copies and saves the current stack frame for later use. On recovery, the stack frame is copied back from its saved location, and the function starts running again. Even if the code may look like a normal sequential blocking request, the coroutine can ensure that the network request does not block the main thread

The two operations of suspending and resuming the process in the main thread not only realize the time-consuming task to be completed by the background thread to ensure the safety of the main thread, but also complete the actual multi-threaded asynchronous call in the way of synchronous code. It can be said that on the Android platform, synergy is mainly used to solve two problems:

  1. Handle long running tasks, which often block the main thread
  2. Ensure that the main thread is safe, that is, ensure that any suspend function is safely called from the main thread

5, CoroutineScope

CoroutineScope is the scope of the collaboration process, which is used to track the collaboration process. If we start multiple processes, but there is no way to manage them uniformly, our code will be bloated and messy, and even memory leakage or task leakage will occur. To ensure that all collaborations are tracked, Kotlin does not allow new collaborations to be started without using CoroutineScope. CoroutineScope can be seen as a lightweight version of ExecutorService with super capabilities. It can start a new collaboration process, and this collaboration process also has the advantages of suspend and resume mentioned above

All collaborations need to be started through CoroutineScope, which will track all collaborations created by launch or async. You can call scope at any time Cancel() cancels the running collaboration. CoroutineScope does not run the process itself. It just ensures that you do not lose track of the process, even if the process is suspended. In Android, some KTX libraries provide their own CoroutineScope for some lifecycle classes. For example, ViewModel has viewModelScope , Lifecycle has lifecycleScope

CoroutineScope can be roughly divided into three types:

  • GlobalScope. That is, the scope of the global collaboration. The collaboration started within this scope can run until the application stops running. Globalscope itself will not block the current thread, and the started coprocess is equivalent to a daemon thread and will not prevent the JVM from ending its operation
  • runBlocking. A top-level function, unlike GlobalScope, blocks the current thread until the execution of all its internal co procedures with the same scope ends
  • Customize CoroutineScope. It can be used to realize the life cycle range of active control collaboration. One of the greatest significance for Android development is to avoid memory leakage

1,GlobalScope

GlobalScope belongs to the global scope, which means that the life cycle of the collaboration started through GlobalScope is only limited by the life cycle of the entire application. As long as the entire application is still running and the task of the collaboration has not ended, the collaboration can run all the time

GlobalScope will not block its thread, so the log of the main thread in the following code will be earlier than the GlobalScope internal output log. In addition, the collaboration started by GlobalScope is equivalent to a daemon thread and will not prevent the JVM from ending running. Therefore, if the sleep time of the main thread is changed to 300 milliseconds, you will not see the launch A output log

fun main() {
    log("start")
    GlobalScope.launch {
        launch {
            delay(400)
            log("launch A")
        }
        launch {
            delay(300)
            log("launch B")
        }
        log("GlobalScope")
    }
    log("end")
    Thread.sleep(500)
}


[main] start
[main] end
[DefaultDispatcher-worker-1 @coroutine#1] GlobalScope
[DefaultDispatcher-worker-3 @coroutine#3] launch B
[DefaultDispatcher-worker-3 @coroutine#2] launch A

GlobalScope.launch will create a top-level collaboration. Although it is very lightweight, it will consume some memory resources when running, and can run until the whole application stops (as long as the task is not finished), which may lead to memory leakage. Therefore, globalscope should be used with caution in daily development

2,runBlocking

The top-level function runBlocking can also be used to start the collaboration. The second parameter of the runBlocking function is the execution body of the collaboration. This parameter is declared as an extension function of CoroutineScope. Therefore, the execution body contains an implicit CoroutineScope, so the collaboration can be started directly inside runBlocking

public fun <T> runBlocking(context: CoroutineContext = EmptyCoroutineContext, block: suspend CoroutineScope.() -> T): T

A convenience of runBlocking is that the code declared after runBlocking can be executed only after all the internal co procedures with the same scope are run, that is, runBlocking will block its thread

Look at the following code. The two collaborations started internally by runBlocking will each do time-consuming operations. From the output results, it can be seen that the two collaborations are still executing alternately and concurrently, and runBlocking will not exit until both collaborations are executed. The external log output results have a clear sequence. That is, the co process started inside runBlocking is non blocking, but runBlocking blocks its thread. In addition, runBlocking will only exit after the collaboration of the same scope is completed, and will not wait for the collaboration started by other scopes such as GlobalScope

Therefore, runBlocking itself means blocking threads, but its internal running processes are non blocking. Readers need to understand the difference between the two

fun main() {
    log("start")
    runBlocking {
        launch {
            repeat(3) {
                delay(100)
                log("launchA - $it")
            }
        }
        launch {
            repeat(3) {
                delay(100)
                log("launchB - $it")
            }
        }
        GlobalScope.launch {
            repeat(3) {
                delay(120)
                log("GlobalScope - $it")
            }
        }
    }
    log("end")
}


[main] start
[main] launchA - 0
[main] launchB - 0
[DefaultDispatcher-worker-1] GlobalScope - 0
[main] launchA - 1
[main] launchB - 1
[DefaultDispatcher-worker-1] GlobalScope - 1
[main] launchA - 2
[main] launchB - 2
[main] end

Based on whether threads will be blocked or not, runBlocking in the following code will be earlier than GlobalScope's output log

fun main() {
    GlobalScope.launch(Dispatchers.IO) {
        delay(600)
        log("GlobalScope")
    }
    runBlocking {
        delay(500)
        log("runBlocking")
    }
    //Active hibernation takes 200 milliseconds, making the delay time combined with runBlocking more than 600 milliseconds
    Thread.sleep(200)
    log("after sleep")
}


[main] runBlocking
[DefaultDispatcher-worker-1] GlobalScope
[main] after sleep

3,coroutineScope

The coroutineScope function is used to create an independent scope of the collaboration and does not end itself until all the started collaborations are completed. runBlocking and coroutineScope look similar because they both need to wait for the end of all their internal collaborations with the same scope. The main difference between the two is that the runBlocking method will block the current thread, while coroutineScope will not block the thread. Instead, it will suspend and release the underlying thread for use by other coroutines. Because of this difference, runBlocking is a normal function, while coroutineScope is a suspended function

fun main() = runBlocking {
    launch {
        delay(100)
        log("Task from runBlocking")
    }
    coroutineScope {
        launch {
            delay(500)
            log("Task from nested launch")
        }
        delay(100)
        log("Task from coroutine scope")
    }
    log("Coroutine scope is over")
}


[main] Task from coroutine scope
[main] Task from runBlocking
[main] Task from nested launch
[main] Coroutine scope is over

4,supervisorScope

The supervisorScope function is used to create a coroutineScope that uses a SupervisorJob. The feature of this scope is that the exceptions thrown will not chain and cancel the peer and parent coroutines

fun main() = runBlocking {
    launch {
        delay(100)
        log("Task from runBlocking")
    }
    supervisorScope {
        launch {
            delay(500)
            log("Task throw Exception")
            throw Exception("failed")
        }
        launch {
            delay(600)
            log("Task from nested launch")
        }
    }
    log("Coroutine scope is over")
}


[main @coroutine#2] Task from runBlocking
[main @coroutine#3] Task throw Exception
[main @coroutine#4] Task from nested launch
[main @coroutine#1] Coroutine scope is over

5. Customize CoroutineScope

Assuming that we have started multiple coroutines in the Activity to perform asynchronous time-consuming operations, when the Activity exits, we must cancel all coroutines to avoid memory leakage. We can manually cancel each Job reference by keeping it in the onDestroy method, but this method is quite cumbersome and inefficient. kotlinx.coroutines provides coroutine scope to manage the life cycle of multiple collaborations

We can manage the lifecycle of a collaboration by creating an instance of the collaboration scope associated with the Activity lifecycle. An instance of CoroutineScope can be built through the factory function of CoroutineScope() or MainScope(). The former creates a common scope, and the latter creates a scope for the UI application and uses dispatchers Main as the default scheduler

class Activity {

    private val mainScope = MainScope()

    fun onCreate() {
        mainScope.launch {
            repeat(5) {
                delay(1000L * it)
            }
        }
    }

    fun onDestroy() {
        mainScope.cancel()
    }

}

Alternatively, we can let the Activity implement the CoroutineScope interface through the delegate mode, so that we can directly start the processes within the Activity without explicitly specifying their context, and automatically cancel all processes in onDestroy()

class Activity : CoroutineScope by CoroutineScope(Dispatchers.Default) {

    fun onCreate() {
        launch {
            repeat(5) {
                delay(200L * it)
                log(it)
            }
        }
        log("Activity Created")
    }

    fun onDestroy() {
        cancel()
        log("Activity Destroyed")
    }

}

It can be seen from the output result that the coroutine will not output the log after calling back the onDestroy() method

fun main() = runBlocking {
    val activity = Activity()
    activity.onCreate()
    delay(1000)
    activity.onDestroy()
    delay(1000)
}


[main @coroutine#1] Activity Created
[DefaultDispatcher-worker-1 @coroutine#2] 0
[DefaultDispatcher-worker-1 @coroutine#2] 1
[DefaultDispatcher-worker-1 @coroutine#2] 2
[main @coroutine#1] Activity Destroyed

A scope that has been canceled cannot create a collaboration again. Therefore, scope. Should only be called if the class that controls its lifecycle is destroyed cancel(). For example, when using viewModelScope, ViewModel Class is automatically de scoped in the onCleared() method of the ViewModel

6, CoroutineBuilder

1,launch

Look at the method signature of the launch function. Launch is an extension function for CoroutineScope. It is used to start a collaboration without blocking the current thread and return a reference to the collaboration task, that is, the Job object

public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job
 Copy code

The launch function contains three parameters:

  1. context. Used to specify the context of the collaboration
  2. start. Used to specify the startup mode of the collaboration. The default value is coroutinestart Default, that is, the collaboration will immediately enter the state of waiting for scheduling at the same time of declaration, that is, the state that can be executed immediately. You can set it to coroutinestart Lazy to achieve delayed startup, that is, lazy loading
  3. block. It is used to transfer the execution body of the collaborative process, that is, the task that you want to be executed by the collaborative process

You can see that launchA and launchB are executed in parallel and cross

fun main() = runBlocking {
    val launchA = launch {
        repeat(3) {
            delay(100)
            log("launchA - $it")
        }
    }
    val launchB = launch {
        repeat(3) {
            delay(100)
            log("launchB - $it")
        }
    }
}


[main] launchA - 0
[main] launchB - 0
[main] launchA - 1
[main] launchB - 1
[main] launchA - 2
[main] launchB - 2

2,Job

Job is a handle to the process. Each collaboration created using launch or async returns a job instance that uniquely identifies the collaboration and manages its lifecycle. Job is an interface type. Here are some useful properties and functions of job

    //true when the Job is active
    //If the Job is not cancelled or fails, it is in active state
    public val isActive: Boolean

    //When the Job ends normally or abnormally, it returns true
    public val isCompleted: Boolean

    //Returns true when the Job is actively cancelled or ends abnormally
    public val isCancelled: Boolean

    //Start Job
    //Returns true if the call did start the Job
    //If the Job is in the started or completed state before calling, false will be returned 
    public fun start(): Boolean

    //Used to cancel a Job. You can also pass in an Exception to indicate the reason for cancellation
    public fun cancel(cause: CancellationException? = null)

    //Block waiting until this Job finishes running
    public suspend fun join()

    //This method is called back when the Job ends running (for whatever reason), which can be used to receive possible running exceptions
    public fun invokeOnCompletion(handler: CompletionHandler): DisposableHandle

Job has the following status values, and the attribute values corresponding to each status are different

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

fun main() {
    //Set the collaboration to delayed start
    val job = GlobalScope.launch(start = CoroutineStart.LAZY) {
        for (i in 0..100) {
            //Each cycle is delayed by 100 milliseconds
            delay(100)
        }
    }
    job.invokeOnCompletion {
        log("invokeOnCompletion: $it")
    }
    log("1. job.isActive: ${job.isActive}")
    log("1. job.isCancelled: ${job.isCancelled}")
    log("1. job.isCompleted: ${job.isCompleted}")

    job.start()

    log("2. job.isActive: ${job.isActive}")
    log("2. job.isCancelled: ${job.isCancelled}")
    log("2. job.isCompleted: ${job.isCompleted}")

    //Sleep for 400 milliseconds and then actively cancel the collaboration
    Thread.sleep(400)
    job.cancel(CancellationException("test"))

    //Sleep for 400 milliseconds to prevent the JVM from stopping too fast, resulting in no time for invokeOnCompletion to callback
    Thread.sleep(400)

    log("3. job.isActive: ${job.isActive}")
    log("3. job.isCancelled: ${job.isCancelled}")
    log("3. job.isCompleted: ${job.isCompleted}")
}


[main] 1. job.isActive: false
[main] 1. job.isCancelled: false
[main] 1. job.isCompleted: false
[main] 2. job.isActive: true
[main] 2. job.isCancelled: false
[main] 2. job.isCompleted: false
[DefaultDispatcher-worker-2] invokeOnCompletion: java.util.concurrent.CancellationException: test
[main] 3. job.isActive: false
[main] 3. job.isCancelled: true
[main] 3. job.isCompleted: true

3,async

Look at the method signature of the async function. Async is also an extension function for CoroutineScope. The main difference between async and launch is that async can return the execution result of the coroutine, but launch cannot

public fun <T> CoroutineScope.async(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> T
): Deferred<T>

The execution results of async coroutines can be obtained through await() method. It can be seen that the total time of the two coroutines is far less than seven seconds, and the total time is basically equal to the longest coroutine

fun main() {
    val time = measureTimeMillis {
        runBlocking {
            val asyncA = async {
                delay(3000)
                1
            }
            val asyncB = async {
                delay(4000)
                2
            }
            log(asyncA.await() + asyncB.await())
        }
    }
    log(time)
}


[main] 3
[main] 4070

Since launch and async can only be used in CouroutineScope, any created collaboration will be tracked by the scope. Kotlin prohibits the creation of processes that cannot be tracked, so as to avoid process leakage

4. Incorrect usage of async

After modifying the above code, it can be found that the total time of the two processes will change to about seven seconds

fun main() {
    val time = measureTimeMillis {
        runBlocking {
            val asyncA = async(start = CoroutineStart.LAZY) {
                delay(3000)
                1
            }
            val asyncB = async(start = CoroutineStart.LAZY) {
                delay(4000)
                2
            }
            log(asyncA.await() + asyncB.await())
        }
    }
    log(time)
}


[main] 3
[main] 7077

This difference is due to coroutine start Lazy does not actively start the coroutine, but until async is called Await () or async Start after satrt() (i.e. lazy loading mode), so asynca. Await() + asyncb Await () causes the two coroutines to execute in sequence. The default value is coroutinestart The default parameter will enable the collaboration process to be started at the same time as it is declared (in fact, it needs to wait for the scheduled execution, but it can be regarded as immediate execution). Therefore, even though async.await() will block the current thread until the collaboration returns the result value, both collaboration processes are actually running, so the total time is about four seconds

At this point, the effect of the first example can be achieved by calling start() and await()

asyncA.start()
asyncB.start()
log(asyncA.await() + asyncB.await())

5. async parallel decomposition

All coroutines started by the suspend function must stop when the function returns the result, so you may need to ensure that these coroutines are completed before the result is returned. With the help of the structured concurrency mechanism in Kotlin, you can define a coroutine scope for starting one or more collaborations. Then, you can use await() (for a single coroutine) or await all() (for multiple coroutines) to ensure that these coroutines are completed before returning the result from the function

For example, suppose we define a coroutine scope for asynchronously retrieving two documents. By calling await() for each deferred reference, we can ensure that these two async operations are completed before the return value:

    suspend fun fetchTwoDocs() =
        coroutineScope {
            val deferredOne = async { fetchDoc(1) }
            val deferredTwo = async { fetchDoc(2) }
            deferredOne.await()
            deferredTwo.await()
    }

You can also use awaitAll() on a collection, as shown in the following example:

suspend fun fetchTwoDocs() =        // called on any Dispatcher (any thread, possibly Main)
    coroutineScope {
        val deferreds = listOf(     // fetch two docs at the same time
            async { fetchDoc(1) },  // async returns a result for the first doc
            async { fetchDoc(2) }   // async returns a result for the second doc
        )
        deferreds.awaitAll()        // use awaitAll to wait for both network requests
}

Although fetchTwoDocs() uses async to start a new coroutine, this function uses awaitAll() to wait for the started coroutine to complete before returning the result. However, please note that even if we do not call awaitAll(), the coroutineScope builder will wait until all new collaborations are completed before resuming the collaboration named fetchTwoDocs. In addition, coroutineScope will catch all exceptions thrown by the coroutine and pass them to the callback user

6,Deferred

The return value of the async function is a deferred object. Deferred is an interface type that inherits from the Job interface, so the Job contains both properties and methods. It mainly extends the await() method on the basis of the Job

7, CoroutineContext

CoroutineContext uses the following set of elements to define the behavior of a collaboration:

  • Job: control the life cycle of the collaboration
  • Coroutine dispatcher: dispatch work to the appropriate thread
  • CoroutineName: the name of the collaboration process, which can be used for debugging
  • CoroutineExceptionHandler: handles uncapped exceptions

1,Job

The Job in the collaboration process is a part of its context CoroutineContext, which can be obtained from the context through the coroutineContext[Job] expression

Although the following two log statements run on different coroutines, they actually point to the same Job object

fun main() = runBlocking {
    val job = launch {
        log("My job is ${coroutineContext[Job]}")
    }
    log("My job is $job")
}


[main @coroutine#1] My job is "coroutine#2":StandaloneCoroutine{Active}@75a1cd57
[main @coroutine#2] My job is "coroutine#2":StandaloneCoroutine{Active}@75a1cd57

In fact, the isActive extension property of CoroutineScope is just coroutinecontext [job] A simple way to write isActive = = true

public val CoroutineScope.isActive: Boolean
    get() = coroutineContext[Job]?.isActive ?: true

2,CoroutineDispatcher

CoroutineContext contains a CoroutineDispatcher, which is used to specify the target carrier for executing the collaboration, that is, which thread to run on. CoroutineDispatcher can limit the execution of the collaboration to a specific thread, dispatch it to the thread pool, or let it run unrestricted. All collaboration constructors Both (such as launch and async) accept an optional parameter, CoroutineContext, which can be used to explicitly specify the CoroutineDispatcher to be used by the collaboration to be created and other context elements

To run code outside the main thread, you can have the Kotlin coroutine execute work on the Default or IO scheduler. In Kotlin, all coroutines must run in coroutine dispatcher, even if they run on the main thread. The coordination process can be suspended by itself, and the coroutine dispatcher is responsible for restoring it

The Kotlin collaboration library provides four dispatchers to specify where to run the collaboration. In most cases, we only contact the following three:

  • Dispatchers.Main - use this scheduler to run a coroutine on the Android main thread. This scheduler can only be used to interact with the interface and perform fast work. Examples include calling the suspend function, running Android interface framework operations, and updating LiveData object

  • Dispatchers.IO -This scheduler is specially optimized to perform disk or network I/O outside the main thread. Examples include using Room components , read data from or write data to a file, and run any network operations

  • Dispatchers.Default - this scheduler is specially optimized to perform CPU intensive work outside the main thread. Examples include sorting lists and parsing JSON

    fun main() = runBlocking {
    launch {
    log("main runBlocking")
    }
    launch(Dispatchers.Default) {
    log("Default")
    }
    launch(Dispatchers.IO) {
    log("IO")
    }
    launch(newSingleThreadContext("MyOwnThread")) {
    log("newSingleThreadContext")
    }
    }

    [DefaultDispatcher-worker-1 @coroutine#3] Default
    [DefaultDispatcher-worker-2 @coroutine#4] IO
    [MyOwnThread @coroutine#5] newSingleThreadContext
    [main @coroutine#2] main runBlocking

When launch {...} When used without parameters, it inherits the context and scheduler from the external collaboration scope, that is, consistent with runBlocking. When starting a collaboration in GlobalScope, the default scheduler is dispatchers Default and uses the shared background thread pool, so launch(Dispatchers.default) {...} With GlobalScope launch{...} Is to use the same scheduler. newSingleThreadContext is used to create a new thread for the co process to run. The special thread is a very expensive resource, which must be released when it is no longer needed in the actual application, or stored in the top-level variable for reuse in the whole application

3,withContext

For the following code, a code block specified to run in the IO thread pool is created using withContext(Dispatchers.IO) in the get method, and any code in this interval is always executed through the IO thread. Since the withContext method itself is a suspended function, the get method must also be defined as a suspended function

suspend fun fetchDocs() {                      // Dispatchers.Main
    val result = get("developer.android.com")  // Dispatchers.Main
    show(result)                               // Dispatchers.Main
}

suspend fun get(url: String) =                 // Dispatchers.Main
    withContext(Dispatchers.IO) {              // Dispatchers.IO (main-safety block)
        /* perform network IO here */          // Dispatchers.IO (main-safety block)
    }                                          // Dispatchers.Main
}

With the help of coroutines, you can schedule threads at a fine granularity. Because withContext() allows you to control the execution thread pool of any code without introducing callbacks, you can apply it to very small functions, such as reading data from the database or executing network requests. A good practice is to use withContext() to ensure that each function is main thread safe, which means that you can call each function from the main thread. In this way, the caller never needs to consider which thread should be used to execute the function

In the previous example, the fetchDocs() method executes on the main thread; However, it can safely call the get method, which will execute the network request in the background. Since the coroutine supports suspend and resume, the coroutine on the main thread will resume immediately according to the get result after the withContext block is completed

Compared with the callback based equivalent implementation, withContext() No additional overhead. In addition, in some cases, the withContext() call can be optimized beyond the callback based equivalent. For example, if a function makes ten calls to a network, you can use the external withContext() to make Kotlin switch threads only once. In this way, even if the network library uses withContext() multiple times, it will remain on the same scheduler and avoid switching threads. In addition, Kotlin optimizes dispatchers Default and dispatchers Switch between IO to avoid thread switching as much as possible

Using a scheduler that uses a thread pool (such as Dispatchers.IO or Dispatchers.Default) cannot guarantee that the code block will always execute from top to bottom on the same thread. In some cases, the Kotlin collaboration may hand over the execution work to another thread after suspend and resume. This means that for the entire withContext() block, Thread local variables may not point to the same value

4,CoroutineName

CoroutineName is used to specify a name for the collaboration process, which is convenient for debugging and locating problems

fun main() = runBlocking<Unit>(CoroutineName("RunBlocking")) {
    log("start")
    launch(CoroutineName("MainCoroutine")) {
        launch(CoroutineName("Coroutine#A")) {
            delay(400)
            log("launch A")
        }
        launch(CoroutineName("Coroutine#B")) {
            delay(300)
            log("launch B")
        }
    }
}


[main @RunBlocking#1] start
[main @Coroutine#B#4] launch B
[main @Coroutine#A#3] launch A

5,CoroutineExceptionHandler

In the following exception handling, we will talk about

6. Combining context elements

Sometimes we need to define multiple elements for the context of a collaboration, so we can use the + operator. For example, we can specify Dispatcher and CoroutineName for the collaboration at the same time

fun main() = runBlocking<Unit> {
    launch(Dispatchers.Default + CoroutineName("test")) {
        log("Hello World")
    }
}


[DefaultDispatcher-worker-1 @test#2] Hello World

In addition, since CoroutineContext is composed of a group of elements, the elements on the right side of the plus sign will overwrite the elements on the left side of the plus sign to form the newly created CoroutineContext. For example, (Dispatchers.Main, "name") + (Dispatchers.IO) = (Dispatchers.IO, "name")

8, Cancel collaboration

If a user exits an Activity/Fragment that has started a collaboration, all the collaborations should be cancelled in most cases

job.cancel() is used to cancel a collaboration, Job join() is used to block and wait for the co process to run. Because the cancel () function will return immediately after calling instead of waiting for the collaboration to return after it is completed, the collaboration may not have stopped running at this time. If you need to ensure that subsequent code is executed after the collaboration ends running, you need to call the join () method to block the wait. You can also complete the same operation by calling the Job extension function cancelAndJoin(), which combines the two operations of cancel and join

fun main() = runBlocking {
    val job = launch {
        repeat(1000) { i ->
            log("job: I'm sleeping $i ...")
            delay(500L)
        }
    }
    delay(1300L)
    log("main: I'm tired of waiting!")
    job.cancel()
    job.join()
    log("main: Now I can quit.")
}


[main] job: I'm sleeping 0 ...
[main] job: I'm sleeping 1 ...
[main] job: I'm sleeping 2 ...
[main] main: I'm tired of waiting!
[main] main: Now I can quit.

1. The collaboration may not be cancelled

Not all collaborative processes can respond to the cancellation operation. The cancellation operation of the collaborative process needs to be completed cooperatively, and the collaborative process can be cancelled only after cooperation. All pending functions in the collaboration library are cancelable. They will check whether the collaboration has been cancelled at run time, and throw cancelationexception when canceling, so as to end the whole task. However, if the collaboration is executing a calculation task and it is not checked whether it is in the cancelled state, the collaboration cannot be cancelled

Therefore, even if the following code cancels the collaboration actively, the collaboration will only end after completing the given cycle, because the collaboration is not checked before each cycle, so the task is not affected by the cancellation operation

fun main() = runBlocking {
    val startTime = System.currentTimeMillis()
    val job = launch(Dispatchers.Default) {
        var nextPrintTime = startTime
        var i = 0
        while (i < 5) {
            if (System.currentTimeMillis() >= nextPrintTime) {
                log("job: I'm sleeping ${i++} ...")
                nextPrintTime += 500L
            }
        }
    }
    delay(1300L)
    log("main: I'm tired of waiting!")
    job.cancelAndJoin()
    log("main: Now I can quit.")
}


[DefaultDispatcher-worker-1] job: I'm sleeping 0 ...
[DefaultDispatcher-worker-1] job: I'm sleeping 1 ...
[DefaultDispatcher-worker-1] job: I'm sleeping 2 ...
[main] main: I'm tired of waiting!
[DefaultDispatcher-worker-1] job: I'm sleeping 3 ...
[DefaultDispatcher-worker-1] job: I'm sleeping 4 ...
[main] main: Now I can quit.

In order to realize the purpose of canceling the collaboration process, it is necessary to add the logic to the above code to judge whether the collaboration process is still in the runnable state, and actively exit the collaboration process when it is not runnable. isActive is an extended property of CoroutineScope, which is used to judge whether the collaboration is still in a runnable state

fun main() = runBlocking {
    val startTime = System.currentTimeMillis()
    val job = launch(Dispatchers.Default) {
        var nextPrintTime = startTime
        var i = 0
        while (i < 5) {
            if (isActive) {
                if (System.currentTimeMillis() >= nextPrintTime) {
                    log("job: I'm sleeping ${i++} ...")
                    nextPrintTime += 500L
                }
            } else {
                return@launch
            }
        }
    }
    delay(1300L)
    log("main: I'm tired of waiting!")
    job.cancelAndJoin()
    log("main: Now I can quit.")
}

Canceling the co operation is similar to calling Thread. in Java. Interrupt () method to initiate an interrupt request to the thread. Neither of these two operations will forcibly stop the collaboration and thread. The external operation is equivalent to initiating a stop request. It needs to rely on the collaboration and thread to actively stop the operation after responding to the request. Kotlin and Java do not provide a method that can directly force the stop of a process or thread, because this operation may bring all kinds of unexpected situations. When stopping processes and threads, they may also hold some exclusive resources (such as locks and database links). If they are stopped forcibly, the locks they hold will not be released, resulting in other processes and threads unable to obtain the target resources, and eventually lead to thread deadlock. Therefore, the Thread.stop() method is also in an abandoned state, Java officials do not provide a reliable way to stop threads

2. finally release resources

Cancelable suspension functions will throw cancelationexception when canceling. You can rely on try {...} finally {...} Or the use function of Kotlin releases the held resources after canceling the coroutine

fun main() = runBlocking {
    val job = launch {
        try {
            repeat(1000) { i ->
                log("job: I'm sleeping $i ...")
                delay(500L)
            }
        } catch (e: Throwable) {
            log(e.message)
        } finally {
            log("job: I'm running finally")
        }
    }
    delay(1300L)
    log("main: I'm tired of waiting!")
    job.cancelAndJoin()
    log("main: Now I can quit.")
}


[main] job: I'm sleeping 0 ...
[main] job: I'm sleeping 1 ...
[main] job: I'm sleeping 2 ...
[main] main: I'm tired of waiting!
[main] StandaloneCoroutine was cancelled
[main] job: I'm running finally
[main] main: Now I can quit.

3,NonCancellable

If the suspend function is called again in the finally block in the previous example, it will cause a cancelationexception to be thrown because the coroutine has been cancelled. We usually don't encounter this situation because common resource release operations are non blocking and do not involve any pending functions. However, in rare cases, we need to call the suspend function in the cancelled coroutine. At this time, we can use the withContext function and NonCancellable context to wrap the corresponding code in the withContext(NonCancellable) {...} In the code block, NonCancellable is used to create a collaboration scope that cannot be cancelled

fun main() = runBlocking {
    log("start")
    val launchA = launch {
        try {
            repeat(5) {
                delay(50)
                log("launchA-$it")
            }
        } finally {
            delay(50)
            log("launchA isCompleted")
        }
    }
    val launchB = launch {
        try {
            repeat(5) {
                delay(50)
                log("launchB-$it")
            }
        } finally {
            withContext(NonCancellable) {
                delay(50)
                log("launchB isCompleted")
            }
        }
    }
    //Delay 100 milliseconds to ensure that both processes have been started
    delay(200)
    launchA.cancel()
    launchB.cancel()
    log("end")
}


[main] start
[main] launchA-0
[main] launchB-0
[main] launchA-1
[main] launchB-1
[main] launchA-2
[main] launchB-2
[main] end
[main] launchB isCompleted

4. Parent and child processes

When a co process is started in the co process scope of another co process, it will pass through coroutinescope Coroutinecontext inherits its context. The newly started collaboration is called a child collaboration, and the Job of the child collaboration will become the child Job of the parent collaboration Job. The parent process will always wait until all its child processes are completed before ending itself, so the parent process does not have to explicitly track all the child processes it starts, nor does it need to use Job The join waits at the end for the subprocess to complete

Therefore, although the delay times of the three sub processes started by parentJob are different, they will eventually print out the log

fun main() = runBlocking {
    val parentJob = launch {
        repeat(3) { i ->
            launch {
                delay((i + 1) * 200L)
                log("Coroutine $i is done")
            }
        }
        log("request: I'm done and I don't explicitly join my children that are still active")
    }
}


[main @coroutine#2] request: I'm done and I don't explicitly join my children that are still active
[main @coroutine#3] Coroutine 0 is done
[main @coroutine#4] Coroutine 1 is done
[main @coroutine#5] Coroutine 2 is done

5. Propagation cancel operation

In general, cancellation of a collaboration is propagated through the hierarchy of the collaboration. If the parent process is cancelled or the parent process throws an exception, the child process will be cancelled. If the fruit collaboration is cancelled, the peer collaboration and parent collaboration will not be affected. However, if the fruit collaboration throws an exception, the peer collaboration and parent collaboration will also be cancelled

For the following code, the cancellation of the child process jon1 does not affect the continued operation of the child process jon2 and the parent process, but after the parent process is cancelled, the child process will be recursively cancelled

fun main() = runBlocking {
    val request = launch {
        val job1 = launch {
            repeat(10) {
                delay(300)
                log("job1: $it")
                if (it == 2) {
                    log("job1 canceled")
                    cancel()
                }
            }
        }
        val job2 = launch {
            repeat(10) {
                delay(300)
                log("job2: $it")
            }
        }
    }
    delay(1600)
    log("parent job canceled")
    request.cancel()
    delay(1000)
}


[main @coroutine#3] job1: 0
[main @coroutine#4] job2: 0
[main @coroutine#3] job1: 1
[main @coroutine#4] job2: 1
[main @coroutine#3] job1: 2
[main @coroutine#3] job1 canceled
[main @coroutine#4] job2: 2
[main @coroutine#4] job2: 3
[main @coroutine#4] job2: 4
[main @coroutine#1] parent job canceled

6,withTimeout

The withTimeout function is used to specify the execution timeout time of the coroutine. If it times out, a timeoutcancelationexception will be thrown to end the operation of the coroutine

fun main() = runBlocking {
    log("start")
    val result = withTimeout(300) {
        repeat(5) {
            delay(100)
        }
        200
    }
    log(result)
    log("end")
}


[main] start
Exception in thread "main" kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 300 ms
    at kotlinx.coroutines.TimeoutKt.TimeoutCancellationException(Timeout.kt:186)
    at kotlinx.coroutines.TimeoutCoroutine.run(Timeout.kt:156)
    at kotlinx.coroutines.EventLoopImplBase$DelayedRunnableTask.run(EventLoop.common.kt:497)
    at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:274)
    at kotlinx.coroutines.DefaultExecutor.run(DefaultExecutor.kt:69)
    at java.lang.Thread.run(Thread.java:748)

The timeoutcancelationexception thrown by withTimeout method is a subclass of cancelationexception. Previously, we did not see the stack information about cancelationexception in the output log, because cancelationexception is considered to be the normal reason for triggering the end of a cancelled process. However, for the withTimeout method, throwing an exception is a means to report the timeout, so the exception will not be digested by the interior of the collaboration

If you don't want the coroutine to end because of an exception, you can use the withTimeoutOrNull method instead. If it times out, it will return null

9, Exception handling

When a coroutine fails due to an exception, it propagates the exception and passes it to its parent coroutine. Next, the parent collaboration will perform the following steps:

  • Cancel its own children
  • Cancel itself
  • Propagate the exception and pass it to its parent

Exceptions will reach the root of the hierarchy, and all collaborations started by CoroutineScope will be cancelled. However, not all collaborations execute the above processes as soon as exceptions are found. launch and async are very different in handling exceptions

launch treats exceptions as uncapped exceptions, similar to Java's thread Uncaughtexceptionhandler, which will be thrown immediately when an exception is found. Async expects to eventually get the result (or exception) by calling await, so it won't throw an exception by default. This means that if you use async to start a new procedure, it silently discards the exception until async. Com is called Await () will get the target value or throw an existing exception

For example, the exception thrown by launchA in the following code will first cause launchB to be cancelled (jobcancelationexception is thrown), and then cause the parent process BlockingCoroutine to be cancelled

fun main() = runBlocking {
    val launchA = launch {
        delay(1000)
        1 / 0
    }
    val launchB = launch {
        try {
            delay(1300)
            log("launchB")
        } catch (e: CancellationException) {
            e.printStackTrace()
        }
    }
    launchA.join()
    launchB.join()
}


kotlinx.coroutines.JobCancellationException: Parent job is Cancelling; job=BlockingCoroutine{Cancelling}@5eb5c224
Caused by: java.lang.ArithmeticException: / by zero
    at coroutines.CoroutinesMainKt$main$1$launchA$1.invokeSuspend(CoroutinesMain.kt:11)
    ···
Exception in thread "main" java.lang.ArithmeticException: / by zero
    at coroutines.CoroutinesMainKt$main$1$launchA$1.invokeSuspend(CoroutinesMain.kt:11)
    ···

1,CoroutineExceptionHandler

If you don't want to print all exception information to the console, you can use CoroutineExceptionHandler as one of the context elements of the coroutine to perform custom logging or exception handling here, which is similar to using thread uncaughtExceptionHandler. However, CoroutineExceptionHandler will only be called on exceptions that are not expected to be handled by the user. Therefore, using it in async has no effect. When an exception occurs inside async and is not caught, async is called Await () will still cause the application to crash

The following code will only catch the exception thrown by launch

fun main() = runBlocking {
    val handler = CoroutineExceptionHandler { _, exception ->
        log("Caught $exception")
    }
    val job = GlobalScope.launch(handler) {
        throw AssertionError()
    }
    val deferred = GlobalScope.async(handler) {
        throw ArithmeticException()
    }
    joinAll(job, deferred)
}


[DefaultDispatcher-worker-2] Caught java.lang.AssertionError

2,SupervisorJob

Cancellation caused by exceptions is a two-way relationship in the collaboration process and will propagate in the whole collaboration hierarchy, but how can we implement one-way cancellation?

For example, suppose that multiple processes are started in an Activity. If the subtask represented by a single process fails, it is not necessary to chain terminate all other process tasks within the entire Activity, that is, it is hoped that the exceptions of the child process will not be propagated to the peer process and the parent process. After the Activity exits, the exception of the parent process (i.e. cancelationexception) should be propagated to all child processes in a chain to terminate all child processes

You can use a supervisor Job to achieve the above effect. It is similar to a conventional Job. The only difference is that the cancellation operation will only propagate downward, and the failure of one sub process will not affect other sub processes

For example, the exception thrown by firstChild in the following example will not cause the secondChild to be cancelled, but when the supervisor is cancelled, the secondChild is also cancelled

fun main() = runBlocking {
    val supervisor = SupervisorJob()
    with(CoroutineScope(coroutineContext + supervisor)) {
        val firstChild = launch(CoroutineExceptionHandler { _, _ -> }) {
            log("First child is failing")
            throw AssertionError("First child is cancelled")
        }
        val secondChild = launch {
            firstChild.join()
            log("First child is cancelled: ${firstChild.isCancelled}, but second one is still active")
            try {
                delay(Long.MAX_VALUE)
            } finally {
                log("Second child is cancelled because supervisor is cancelled")
            }
        }
        firstChild.join()
        log("Cancelling supervisor")
        //Cancel all collaborations
        supervisor.cancel()
        secondChild.join()
    }
}


[main] First child is failing
[main] First child is cancelled: true, but second one is still active
[main] Cancelling supervisor
[main] Second child is cancelled because supervisor is cancelled

However, if the exception is not handled and the CoroutineContext does not contain a CoroutineExceptionHandler, the exception will reach the exception handler of the default thread. In the JVM, exceptions are printed on the console; In Android, no matter which Dispatcher the exception occurs in, it will directly cause the application to crash. Therefore, if the CoroutineExceptionHandler contained in firstChild is removed in the above example, the Android application will crash

💥 Exceptions that are not caught must be thrown, no matter what kind of Job is used

10, Android KTX

Android KTX is included in Android Jetpack And a set of Kotlin extensions in other Android libraries. KTX extensions can provide concise and idiomatic Kotlin code for Jetpack, Android platform and other API s. To this end, these extensions make use of a variety of Kotlin language functions, including support for Kotlin collaboration

1,ViewModel KTX

The ViewModel KTX library provides a viewModelScope, which is used to start the collaboration in the ViewModel. The life cycle of the scope is equal to that of the ViewModel. When the ViewModel calls back the onCleared() method, all the collaboration in the current ViewModel will be automatically cancelled

Import dependency:

    dependencies {
        implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0"
    }

For example, the following fetchDocs() method starts a coroutine relying on viewModelScope to initiate network requests in the background thread

class MyViewModel : ViewModel() {
    
    fun fetchDocs() {
        viewModelScope.launch {
            val result = get("https://developer.android.com")
            show(result)
        }
    }

    suspend fun get(url: String) = withContext(Dispatchers.IO) { /* ... */ }

}

2,Lifecycle KTX

Lifecycle KTX for each Lifecycle The object defines a LifecycleScope. The scope has the guarantee of life cycle security. The collaboration started within this scope will be cancelled at the same time when the Lifecycle is destroyed. You can use Lifecycle CoroutineScope or Lifecycle owner Use the LifecycleScope attribute to get the CoroutineScope

Import dependency:

    dependencies {
        implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.2.0"
    }

The following example demonstrates how to use lifecycle owner Lifecycle scope asynchronously creates precomputed text:

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)
        }
    }
    
}

3,LiveData KTX

When using LiveData, you may need to calculate values asynchronously. For example, you may need to retrieve a user's preferences and send them to the interface. In these cases, LiveData KTX provides a LiveData builder function that calls the suspend function and assigns the result to LiveData

Import dependency:

    dependencies {
        implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.2.0"
    }

In the following example, loaduser () is a suspend function declared elsewhere. You can use the liveData builder function to asynchronously call loadUser(), and then use emit() to issue the result:

val user: LiveData<User> = liveData {
    val data = database.loadUser() // loadUser is a suspend function.
    emit(data)
}

11, Turn from Nuggets

Topics: Android kotlin