Kotlin process I - Coroutine

Posted by yeshuawatso on Sat, 15 Jan 2022 18:59:42 +0100

Python wechat ordering applet course video

https://edu.csdn.net/course/detail/36074

Python practical quantitative transaction financial management system

https://edu.csdn.net/course/detail/35475
Kotlin collaboration series article navigation:
Kotlin process I - Coroutine
Kotlin collaboration process 2 - Channel
Kotlin collaboration process III - data Flow
Kotlin collaboration process IV -- Application of Flow and Channel
Kotlin collaboration 5 - using kotlin collaboration in Android

Catalogue* 1, Some pre knowledge of collaborative process
+ 1.1 processes and threads
- 1.1.1 basic definitions
- 1.1.2 why are there threads
- 1.1.3 difference between process and thread
+ 1.2 cooperative and preemptive
- 1.2.1 collaborative
- 1.2.2 preemptive
+ 1.3 coordination process

1, Some pre knowledge of collaborative process

1.1 processes and threads

1.1.1 basic definitions

process
Process is a dynamic execution process of a program with certain independent functions on a data set. It is an independent unit for resource allocation and scheduling by the operating system and the carrier of application program.
Process is the smallest unit of resource allocation. In a single core CPU, only one program is called and run by the CPU in memory at the same time.

thread
The basic CPU execution unit, the smallest unit in the process of program execution, is composed of thread ID, program counter, register combination and stack.
The introduction of thread reduces the overhead of program concurrent execution and improves the concurrent performance of the operating system.

1.1.2 why are there threads

  1. A single process can only do one thing, and the code in the process is still executed serially.
  2. If the execution process is blocked, the whole process will hang. Even if some work in the process does not depend on the waiting resources, it will not be executed.
  3. The memory between multiple processes cannot be shared, and the communication between processes is troublesome

1.1.3 difference between process and thread

  1. A program has at least one process, and a process has at least one thread. The process can be understood as a container for threads;
  2. The process has an independent memory unit during execution, and multiple threads in the process share memory;
  3. Processes can be extended to multiple machines, and threads are suitable for multiple cores at most;
  4. Each independent thread has an entry, sequential execution column and program exit for program operation, but it cannot run independently. It needs to depend on the application, and the application provides multiple thread execution control;
  5. "Process" is the smallest unit of "resource allocation", and "thread" is the smallest unit of "CPU scheduling"
  6. Both processes and threads are descriptions of a time period, which is the description of the CPU working time period, but the particle size is different.

1.2 cooperative and preemptive

1.2.1 collaborative

The early operating system adopted cooperative multitasking, that is, the process actively ceded the execution right. If the current process needs to wait for IO operation, it actively ceded the CPU and the system schedules the next process.
Question:

  1. Rogue application processes always occupy cpu and do not give up resources
  2. The robustness of a process program is poor, and there are problems such as dead cycle and deadlock, resulting in the paralysis of the whole system.

1.2.2 preemptive

The operating system decides the execution right. The operating system has the ability to take control from any process and make another process gain control. The system allocates time slices to each process fairly and reasonably. When the process runs out, it sleeps. Even if the time slice is not used up, but there are more urgent events to be implemented first, it will also force the process to sleep.

With the experience of process design, threads are also made into preemptive multitasking, but it also brings a new thread safety problem. This problem is generally solved by locking, which will not be carried out here.

1.3 coordination process

Go, Python and many other languages have implemented the collaboration process at the language level. java also has a three-party library to implement the collaboration process, but it is not commonly used. Kotlin implements the collaboration process at the language level. Compared with java, it is mainly used to solve the pain point of asynchronous task thread switching.

Coprocessing is based on threads, but it is much lighter than threads. It can be understood as simulating thread operation in the user layer;
Each collaboration process is dynamically bound with a kernel thread. Scheduling and switching are realized in user mode. The kernel thread is the one that really executes the task.
The kernel is required to participate in the context switching of threads, and the context switching of CO processes is completely controlled by the user, which avoids a large number of interrupt participation and reduces the resources consumed by thread context switching and scheduling.
Thread is a concept at the operating system level, and coroutine is a concept at the language level

The biggest difference between threads and coroutines is that threads are passively suspended for recovery, and coroutines are actively suspended for recovery.

A non preemptive (cooperative) task scheduling mode in which programs can actively suspend or resume execution.

Essentially, a coroutine is a lightweight thread—— kotlin Chinese document

I think this concept is a little vague ------- it leads people into misunderstanding. Later.

In the "fake" collaboration, Kotlin does not implement a synchronization mechanism (lock) at the language level, but still relies on Java keywords (such as synchronized) provided by Kotlin JVM, that is, the implementation of lock is still handed over to threads
Therefore, the Kotlin coroutine is essentially a set of encapsulation based on the native Java thread pool.
The core competitiveness of Kotlin collaboration is that it can simplify asynchronous concurrent tasks and write asynchronous code in a synchronous manner.

2, Basic use of Kotlin collaboration

Before talking about concepts, talk about usage.

Scenario: start the worker thread to execute a time-consuming task, and then process the results in the main thread.

Common handling methods:

  • Define your own callback for processing
  • Using thread / thread pool, Callable
    Thread (featuretask (callable)) start
    Thread pool submit(Callable)
  • Android: Handler, AsyncTask, Rxjava

Use collaboration:

coroutineScope.launch(Dispatchers.Main) { // Start a coroutine in the main thread
    val result = withContext(Dispatchers.Default) { // Switch to child thread execution
        doSomething()  // Time consuming task
    }
    handResult(result)  // Switch back to the main thread for execution
}

Note here: dispatchers Main is unique to Android. If it is used in java programs, an exception will be thrown.

2.1 three ways to create a collaborative process

  1. Use the runBlocking top-level function to create:
runBlocking {
 ...
}

  1. Creating with GlobalScope singleton objects
GlobalScope.launch {
 ...
}

  1. Create a CoroutineScope object by yourself through CoroutineContext
val coroutineScope = CoroutineScope(context)
coroutineScope.launch {
    ...
}

  • Method 1 is usually applicable to the scenario of unit testing, which will not be used in business development because it is thread blocked.
  • The difference between method 2 and using runBlocking is that it does not block threads. However, this usage is also not recommended in Android development, because its life cycle will only be limited by the life cycle of the whole application and cannot be cancelled.
  • The third method is the recommended use method. We can manage and control the life cycle of the collaboration process through the context parameter (the context here is not the same thing as that in Android, but a more general concept, which will be encapsulated by an Android platform).

2.2 waiting for a job

Let's take a look at an example:

fun main() = runBlocking {
    launch {
        delay(100)
        println("hello")
        delay(300)
        println("world")
    }
    println("test1")
    println("test2")
}

The results are as follows:

test1
test2
hello
world

After we start a coroutine, we can keep the reference to it and wait for its execution to end. Note that the wait here is non blocking and will not suspend the current thread.

fun main() = runBlocking {
    val job = launch {
        delay(100)
        println("hello")
        delay(300)
        println("world")
    }
    println("test1")
    job.join()
    println("test2")
}

Output results:

test1
hello
world
test2

Similar to java threads, there are also join methods. However, threads are different from the operating system. On some CPUs, the join method may not work.

2.3 cancellation of cooperation

Compared with threads, java threads do not provide any mechanism to terminate threads safely.
The Thread class provides a method interrupt() method, which is used to interrupt the execution of the Thread. Calling the interrupt() method does not mean stopping the work of the target Thread immediately, but just passing a message requesting an interrupt. The Thread then interrupts itself at the next appropriate time.

However, the coroutine provides a cancel() method to cancel the job.

fun main() = runBlocking {
    val job = launch {
        repeat(1000) { i ->
            println("job: test $i ...")
            delay(500L)
        }
    }
    delay(1300L) // Delay for some time
    println("main: ready to cancel!")
    job.cancel() // Cancel the job
    job.join() // Wait for job execution to end
    println("main: Now cancel.")
}

Output results:

job: test 0 ...
job: test 1 ...
job: test 2 ...
main: ready to cancel!
main: Now cancel.

You can also use the function cancelAndJoin, which combines calls to cancel and join.

Question:
If you call job After join (), job. is called. What is the case with cancel()?

Cancellation is collaborative
A collaboration process is not necessarily cancelled. The cancellation of a collaboration process is collaborative. A piece of collaboration code must cooperate before it can be cancelled.
All kotlinx All pending functions in coroutines can be cancelled. They check the cancellation of the collaboration and throw a cancelationexception when canceling.
If a collaboration is executing a computing task and does not check for cancellation, it cannot be cancelled.

fun main() = runBlocking {
    val startTime = System.currentTimeMillis()
    val job = launch(Dispatchers.Default) {
        var nextPrintTime = startTime
        var i = 0
        while (i < 5) { // A loop that performs calculations just to occupy the CPU
            // Print messages twice per second
            if (System.currentTimeMillis() >= nextPrintTime) {
                println("job: hello ${i++} ...")
                nextPrintTime += 500L
            }
        }
    }
    delay(1300L) // Wait for a while
    println("main: ready to cancel!")
    job.cancelAndJoin() // Cancel a job and wait for it to finish
    println("main: Now cancel.")
}

Print results at this time:

job: hello 0 ...
job: hello 1 ...
job: hello 2 ...
main: ready to cancel!
job: hello 3 ...
job: hello 4 ...
main: Now cancel.

It can be seen that the collaboration has not been cancelled. In order to really stop the cooperation process, we need to regularly check whether the cooperation process is active.

Check job status
One method is to add code to check the status of the collaboration in while (I < 5)
The code is as follows:

while (i < 5 && isActive)

This means that our work will be executed only when the collaboration is in the active state.

Another method is to use the function ensureActive() in the standard library of coprocessing. Its implementation is as follows:

public fun Job.ensureActive(): Unit {
    if (!isActive) throw getCancellationException()
}

The code is as follows:

while (i < 5) { // A loop that performs calculations just to occupy the CPU
    ensureActive()
    ...
}

ensureActive() throws an exception immediately when the coroutine is not in the active state.

Use yield()
yield() is used in the same way as ensureActive.
The first Job that yield will do is to check whether the task is completed. If the Job has been completed, it will throw a cancelationexception to end the process. Yield should be called first in the timing check.

while (i < 5) { // A loop that performs calculations just to occupy the CPU
    yield()
    ...
}

2.4 waiting for the execution results of the cooperation process

For a coroutine with no return value, it is created using the launch function. If the return value is required, it is created through the async function.
Using async method to start Deferred (also a job), you can call its await() method to get the execution result.
The following code is shown:

val asyncDeferred = async {
    ...
}

val result = asyncDeferred.await()

Deferred can also be canceled. For deferred that has been canceled, calling the await() method will throw
Jobcancelationexception exception.

Similarly, in deferred After await, call deferred.. Cancel (), then nothing will happen because the task is over.

The specific usage of async will be discussed later.

2.5 exception handling of coordination process

Because CancellationException will be thrown when the collaboration is cancelled, we can wrap the pending function in the try/catch code block, so that we can clean up resources in the finally code block.

fun main() = runBlocking {
    val job = launch {
        try {
            delay(100)
            println("try...")
        } catch (e: Exception) {
            println("exception: ${e.message}")
        } finally {
            println("finally...")
        }
    }
    delay(50)
    println("cancel")
    job.cancel()
    print("Done")
}

result:

cancel
Doneexception: StandaloneCoroutine was cancelled
finally...

2.6 timeout of coordination process

In practice, most of the reasons for canceling a collaborative process are that it may time out. When you manually track the reference of a related Job and start it, use the withTimeout function.

fun main() = runBlocking {
    withTimeout(300) {
        println("start...")
        delay(100)
        println("progress 1...")
        delay(100)
        println("progress 2...")
        delay(100)
        println("progress 3...")
        delay(100)
        println("progress 4...")
        delay(100)
        println("progress 5...")
        println("end")
    }
}

result:

start...
progress 1...
progress 2...
Exception in thread "main" kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 300 ms


withTimeout threw timeoutcancelationexception, which is a subclass of cancelationexception. We haven't seen stack trace information printed on the console before. This is because CancellationException is considered as the normal reason for the end of the execution of the cancelled process. However, in this example, we used withTimeout correctly in the main function. If necessary, we need to actively handle catch exceptions.

Of course, there is another way: use withTimeoutOrNull.

withTimeout can be returned by. When the withTimeout function is executed, it will block and wait for the return result after execution or throw an exception when it times out. The usage of withTimeoutOrNull is the same as withTimeout, except that null is returned after timeout.

3, Concurrent and pending functions

3.1 using async concurrency

Consider a scenario: start multiple tasks and execute them concurrently. After all tasks are executed, return the results, summarize the results and continue to execute.
There are many solutions for this scenario, such as java's FeatureTask, CountDownLatch and semaphore in the concurrent package, and the Zip transformation operation provided by rxjava.

As mentioned earlier, we usually use the async function to start a coroutine with a return value.

Here is a code:

fun main() = runBlocking {
    val time = measureTimeMillis {
        val a = async(Dispatchers.IO) {
            printWithThreadInfo()
            delay(1000) // Simulate time-consuming operations
            1
        }
        val b = async(Dispatchers.IO) {
            printWithThreadInfo()
            delay(2000) // Simulate time-consuming operations
            2
        }
        printWithThreadInfo("${a.await() + b.await()}")
        printWithThreadInfo("end")
    }
    printWithThreadInfo("time: $time")
}

Execution results:

thread id: 12, thread name: DefaultDispatcher-worker-1 ---> 
thread id: 14, thread name: DefaultDispatcher-worker-3 ---> 
thread id: 1, thread name: main ---> 3
thread id: 1, thread name: main ---> end
thread id: 1, thread name: main ---> time: 2051

After async starts a coopera tion, the await method will be blocked and the result will be returned.

3.2 lazy start async

async can start by setting the start parameter to coroutinestart Lazy becomes inert. In this mode, the collaboration will start only when await is called to obtain the execution result of the collaboration, or when the start method of Job is called.

fun main() = runBlocking {
    val time = measureTimeMillis {
        val a = async(Dispatchers.IO, CoroutineStart.LAZY) {
            printWithThreadInfo()
            delay(1000) // Simulate time-consuming operations
            1
        }
        val b = async(Dispatchers.IO, CoroutineStart.LAZY) {
            printWithThreadInfo()
            delay(2000) // Simulate time-consuming operations
            2
        }
        a.start()
        b.start()
        printWithThreadInfo("${a.await() + b.await()}")
        printWithThreadInfo("end")
    }
    printWithThreadInfo("time: $time")
}

Execution results:

thread id: 14, thread name: DefaultDispatcher-worker-3 ---> 
thread id: 12, thread name: DefaultDispatcher-worker-1 ---> 
thread id: 1, thread name: main ---> 3
thread id: 1, thread name: main ---> end
thread id: 1, thread name: main ---> time: 2037

Imagine what happens if the start() method is not shown?

3.3 suspend function

In the above example, let's extract the calculation process of task a into a function. As follows:

fun main() = runBlocking {
    val time = measureTimeMillis {
        val a = async(Dispatchers.IO) {
            calA()
        }
        val b = async(Dispatchers.IO) {
            printWithThreadInfo()
            delay(2000) // Simulate time-consuming operations
            2
        }
        printWithThreadInfo("${a.await() + b.await()}")
        printWithThreadInfo("end")
    }
    printWithThreadInfo("time: $time")
}

fun calA(): Int {
    printWithThreadInfo()
    delay(1000) // Simulate time-consuming operations
    return 1
}

At this time, you will find that the compiler reports an error.

delay(1000) // Simulate time-consuming operations

The error reported in this line is: Suspend function 'delay' should be called only from a coroutine or another suspend function
The pending function delay should be called in another pending function.

View the source code of delay function:

public suspend fun delay(timeMillis: Long) {
    if (timeMillis <= 0) return // don't delay
    return suspendCancellableCoroutine sc@ { cont: CancellableContinuation<Unit> ->
        // if timeMillis == Long.MAX\_VALUE then just wait forever like awaitCancellation, don't schedule.
        if (timeMillis < Long.MAX_VALUE) {
            cont.context.delay.scheduleResumeAfterDelay(timeMillis, cont)
        }
    }
}

You can see that the method signature is decorated with suspend, indicating that the function is a suspended function. To solve this exception, we only need to modify the Calla () method we defined with suspend to make it a suspended function.

A function decorated with the suspend keyword becomes a suspended function. A suspended function can only be called in another suspended function or a coroutine. Normal functions (non suspended functions) can be called in suspended functions.

3.4 nature of coordination and suspension

3.4.1 what is the collaborative process

Kotlin Chinese documents say that, in essence, a coroutine is a lightweight thread. As I said earlier, this concept is a little vague. The implementation of kotlin coprocess is based on threads, which can be understood as an encapsulation framework for threads. Start a coroutine. Use the launch or async function to start the closure code block in the function. For example, starting a thread is to execute the code in the run method. Therefore, the coroutine can be understood as this code block.
The core of a coroutine is that a function or a program can be suspended and restored at the suspended position later.

3.4.2 what does hanging mean

What does it mean to hang up in the process?
suspend translates to interrupt or pause. When I first came into contact with this concept, I felt that hanging means that the code execution stopped here. This is wrong.

We should understand it in the coroutine that when the thread executes the suspend function of the coroutine, it will not continue to execute the coroutine code for the time being. This suspension is for the current thread. Suspending from the current thread means that the collaboration is separated from the thread executing it. It does not mean that the collaboration has stopped, but that the current thread no longer cares what the collaboration is going to do.

When the coprocessor executes the suspended function, it will break away from the current thread and continue to execute. At this time, the thread to execute is specified by the coprocessor scheduler. After the suspended function is executed, it will switch back to its original thread. This is the advantage of collaborative process.

Understand the difference between a coroutine and a thread:

  • Once the thread starts executing, it will not pause until the task ends. This process is continuous. The thread is preemptive scheduling, and there is no problem of cooperation.
  • The cooperative program can suspend and recover by itself, and the program can handle the suspension and recovery by itself, so as to realize the cooperative scheduling of the program execution process.

The so-called hang in Kotlin is a thread scheduling operation that will be automatically cut back later. This resume function is a coroutine. If it is not called in the coroutine, it cannot be recovered. Therefore, the pending function must be called in a coroutine or another pending function. Always called directly or indirectly in a coroutine.

3.5 how to implement the suspend function

The purpose of suspending is to separate the program from the current thread, that is, to cut the thread. The kotlin coroutine provides a withContext() method to realize thread switching.

private suspend fun calB(): Int {
    withContext(Dispatchers.IO) {
        printWithThreadInfo()
    }
    return 2
}

withContext() itself is also a suspend function. It receives a Dispatcher parameter. Depending on this parameter, the coroutine is suspended and switched to other threads. Therefore, if you want to write a suspend function yourself, in addition to adding the suspend keyword to close the market, you also need to call the suspend function of the Kotlin coroutine framework directly or indirectly within the function. For example, the delay function called earlier actually cuts threads inside the framework.

3.5.1 meaning of suspend

Suspend does not switch threads. Thread cutting depends on the actual code in the suspend function. This keyword is just a reminder. If I create a suspend function without other suspend functions, the compiler will also prompt that this modifier is redundant.

suspend indicates that this function is a suspended function, which limits it to be called only in a coroutine or other suspended functions.

Other languages, such as C #, use the async keyword.

3.5.2 how to define a suspend function

If a function is time-consuming, you can define it as a suspended function. There are generally two cases of time consumption: I/O operation and CPU computing.
In addition, there is a delay operation, which can also be defined as a suspended function. The execution of the code itself is not time-consuming, but it needs to be delayed for a period of time.

Writing method
Add the suspend keyword to the function. If it is a time-consuming operation, you can operate the content of the function in withContext. If it is a delayed operation, you can call the delay function.
Delayed operation:

suspend fun testA() {
    ...
    delay(1000)
    ...
}

Time consuming operations:

suspend fun testB() {
    withContext(Dispatchers.IO) {
        ...
    }
}

It can also be written as:

suspend fun testB() = withContext(Dispatchers.IO) {
    ...
}

4, Context and scope of the collaboration

Two concepts:

  • CoroutineContext the context of a coroutine
  • Scope of CoroutineScope collaboration

4.1 CoroutineContext

Coroutines always run in some contexts represented by CoroutineContext type. A coroutine context is a collection of different elements. The main element is the Job in the collaboration and its scheduler.

The collaboration context contains the information of the current collaboration scope, such as Job, ContinuationInterceptor, CoroutineName and CoroutineId. In CoroutineContext, map is used to store the information. The key of map is the associated object of these classes, and the value is an instance of these classes. You can get the information of context in this way:

val job = context[Job]
val continuationInterceptor = context[ContinuationInterceptor]

Job inherits coroutinecontext Element´╝îCoroutineContext.Element inherits coroutinecontext. The context is part of his process. Abstract coroutine, an important subclass of job, is a collaborative process. Using the launch or async method will instantiate a AbstractCoroutine collaboration object. The job value of a collaboration context is itself.

val job = mScope.launch {
        printWithThreadInfo("job: ${this.coroutineContext[Job]}")
    }
    printWithThreadInfo("job2: $job")
    printWithThreadInfo("job3: ${job[Job]}")

Output:

thread id: 1, thread name: main ---> job2: StandaloneCoroutine{Active}@1ee0005
thread id: 12, thread name: test\_dispatcher ---> job: StandaloneCoroutine{Active}@1ee0005
thread id: 1, thread name: main ---> job3: StandaloneCoroutine{Active}@1ee0005

The collaboration context contains a coroutine dispatcher, which determines which thread or threads the relevant collaboration is executed on. The collaboration scheduler can limit the execution of a collaboration to a specific thread, assign it to a thread pool, or let it run unrestricted.
All collaboration builders such as launch and async receive an optional CoroutineContext parameter, which can be used to explicitly specify a scheduler for a new collaboration or other context elements.
When launch {...} is called, no parameters are passed. It inherits the context (and scheduler) from the CoroutineScope that started it.

The two most important information of CoroutineContext are Dispatcher and Job, and Dispatcher and Job themselves implement the interface of CoroutineContext. Is its subclass.
This design is very interesting.

Sometimes we need to define multiple elements in the context of a collaboration. We can use the + operator. For example, we can explicitly specify a scheduler to start the process and specify a name at the same time:

launch(Dispatchers.Default + CoroutineName("test")) {
    println("I'm working in thread ${Thread.currentThread().name}")
}

This is due to the fact that CoroutineContext overloads the operator +.

4.2 CoroutineScope

CoroutineScope refers to the scope of CO process operation. Its source code is as follows:

public interface CoroutineScope {
    public val coroutineContext: CoroutineContext
}

It can be seen that the code of CoroutineScope is very simple. Its main function is to provide CoroutineContext. CoroutineContext is required to start the collaborative process.
A scope can manage all collaborations within its scope. A CoroutineScope can have many sub scopes. Within the collaboration process, it is through CoroutineScope CoroutineContext automatically inherits from the context of the parent coroutine. CoroutineContext is a shortcut to switch threads for a coroutine within the scope.

Note: when GlobalScope is used to start a collaboration, the job of the new collaboration has no parent job. Therefore, it is independent of the scope of this startup and operates independently. GlobalScope contains the EmptyCoroutineContext.

  • A parent process always waits for the execution of all child processes to end. The parent process does not explicitly track the start of all child processes, and it is not necessary to use job Join waits for them at the end.
  • Canceling the parent process cancels all child processes. Therefore, Scope is used to manage the lifecycle of the collaboration process.
  • By default, a child process throws a non CancellationException in the process, which is not caught and will be passed to the parent process. If any child process exits abnormally, the whole process will exit

4.3 create CoroutineScope

To create a CoroutineScope, simply call the public fun CoroutineScope (context: coroutinescontext) method and pass in a coroutinescontext object.

Within the scope of a collaboration, a child collaboration is started, and the context of the parent collaboration is automatically inherited by default. However, when starting, we can specify the incoming context.

val dispatcher = Executors.newFixedThreadPool(1).asCoroutineDispatcher()
val myScope = CoroutineScope(dispatcher)
myScope.launch {
    ...
}

4.4 SupervisorJob

Start a collaboration. By default, the instantiation is of Job type. Under this type, a child process throws a non CancellationException in the process, which is not caught and will be passed to the parent process. If any child process exits abnormally, the whole will exit.
In order to solve the above problems, you can use SupervisorJob instead of Job. SupervisorJob is basically similar to Job, except that it will not be affected by the exceptions of sub processes.

private val svJob = SupervisorJob()
private val mDispatcher = newSingleThreadContext("test\_dispatcher")

private val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
    printWithThreadInfo("exceptionHandler: throwable: $throwable")
}

private val svScope = CoroutineScope(svJob + mDispatcher + exceptionHandler)
private val mScope = CoroutineScope(Job() + mDispatcher + exceptionHandler)

svScope.launch {
    ...
}

// perhaps
supervisorScope { 
    launch { 
        ...
    }
}

4.5 how to use coprocessor in Android

4.5.1 customize coroutineScope

Do not use GlobalScope to start a collaboration, because the lifecycle of the collaboration started by GlobalScope is consistent with that of the application and cannot be cancelled. It is officially recommended to customize the scope of collaboration in Android. Of course, Kotlin provides us with MainScope, which we can use directly.

public fun MainScope(): CoroutineScope = ContextScope(SupervisorJob() + Dispatchers.Main)

Then let the Activity implement the scope:

class BasicCorotineActivity : AppCompatActivity(), CoroutineScope by MainScope() {
    ...
}

Then start the process through launch or async

private fun loadAndShow() {
    launch {
        val task = async(Dispatchers.IO) {
            // load process
            delay(3000)
            ...
            "hello, kotlin"
        }
        tvShow.setText(task.await())
    }
}

Finally, don't forget to cancel the collaboration during Activity onDestory.

override fun onDestroy() {
    cancel()
    super.onDestroy()
}

4.5.2 ViewModelScope

If you use ViewModel + LiveData to implement MVVM architecture, you won't write any logic code on the Activity, let alone start the collaboration. At this time, most of the work will be handed over to ViewModel. So how to define the scope of a collaboration in the ViewModel? Just move the upper MainScope() directly.

class ViewModelOne : ViewModel() {

    private val viewModelJob = SupervisorJob()
    private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)

    val mMessage: MutableLiveData = MutableLiveData()

 fun getMessage(message: String) {
 uiScope.launch {
 val deferred = async(Dispatchers.IO) {
 delay(2000)
 "post $message"
 }
 mMessage.value = deferred.await()
 }
 }

 override fun onCleared() {
 super.onCleared()
 viewModelJob.cancel()
 }
}

uiScope here is actually equivalent to MainScope. Calling the getMessage() method has the same effect as the previous loadAndShow(). Remember to cancel the collaboration in the onCleared() callback of ViewModel.

You can define a BaseViewModel to handle these logic and avoid writing template code repeatedly.

However, Kotlin provided ViewModel KTX. Introduce the following dependencies:

implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0-alpha03"

Then you can directly use the collaboration scope viewModelScope. viewModelScope is an extended property of ViewModel, which is defined as follows:

val ViewModel.viewModelScope: CoroutineScope
    get() {
        val scope: CoroutineScope? = this.getTag(JOB_KEY)
        if (scope != null) {
            return scope
        }
        return setTagIfAbsent(JOB_KEY,
            CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main))
    }

Therefore, using viewModelScope directly is the best choice.

4.5.3 LifecycleScope

LifecycleScope is also matched with viewModelScope to introduce dependencies:

implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.2.0-alpha03"

lifecycle-runtime-ktx defines the scope lifecycleScope for each LifeCycle object through extended attributes. You can use LifeCycle Coroutinescope or LifeCycle owner Access lifecycleScope. The example code is as follows:

lifecycleOwner.lifecycleScope.launch {
    val deferred = async(Dispatchers.IO) { 
        getMessage("LifeCycle Ktx")
    }
    mMessage.value = deferred.await()
}

When the LifeCycle callback onDestroy(), the scope lifecycleScope will be automatically cancelled.

5, Data synchronization in concurrent processes

5.1 thread data security

Classic example:

var flag = true

fun main() {
    Thread {
        Thread.sleep(1000)
        flag = false
    }.start()
    while (flag) {
    }
}

The program did not exit after one second, as we expected, but was in a loop all the time.

Add volatile key modification to flag:

@Volatile
var flag = true

Before modifying the flag with volatile, the invisibility is changed. After one thread changes its value, the other thread "doesn't know", so the program doesn't exit. When a variable is declared as volatile, both the compiler and the runtime will notice that the variable is shared, so the operations on the variable will not be reordered together with other memory operations. Volatile variables are not cached in registers or invisible to other processors, so the latest written value is always returned when reading variables of volatile type.

When accessing the volatile variable, the locking operation will not be performed, so the execution thread will not be blocked. Therefore, the volatile variable is a lighter synchronization mechanism than the synchronized keyword.

When reading and writing non volatile variables, each thread first copies the variables from memory to the CPU cache. If the computer has multiple CPUs, each thread may be processed on a different CPU, which means that each thread can be copied to a different CPU cache.

The declared variable is volatile, and the JVM ensures that the variable is read from memory every time it is read, skipping the step of CPU cache.

The traversal of volatile modification has the following characteristics:

  1. Ensure the visibility of this variable to all threads. When a thread modifies the value of this variable, volatile ensures that the new value can be synchronized to the main memory immediately and refreshed from the main memory immediately before each use. However, ordinary variables cannot do this. The value of ordinary variables needs to be transferred between processes through main memory (see Java Memory Model for details).
  2. Prohibit instruction reordering optimization.
  3. Threads are not blocked.

If you print a line in the while loop, even if you remove the volatile modifier, you can exit the program and check the println() source code. Finally, you find that there is a synchronous code block in it,

synchronized (this) {
    ensureOpen();
    textOut.newLine();
    textOut.flushBuffer();
    charOut.flushBuffer();
    if (autoFlush)
        out.flush();
}

So the question is, what did synchronized do..
Logically, synchronized only ensures the visibility of the variables in the synchronization block and synchronizes them to the main memory immediately after changes occur. However, the flag variable is not in the synchronization block. In fact, the JVM optimizes modern machines to the greatest extent, that is, ensures the timely synchronization between threads and main memory to the greatest extent, That is, the virtual machine adds a volatile as much as possible. However, when the CPU is occupied all the time, the synchronization will not be timely, and the background thread will not end all the time.

5.2 data synchronization in coordination

Take the following example:

class Test {
    private var count = 0
    suspend fun test() = withContext(Dispatchers.IO) {
        repeat(100) {
            launch {
                repeat(1000) {
                    count++
                }
            }
        }
        launch {
            delay(3000)
            printWithThreadInfo("end count: $count")
        }
    }
}

fun main() = runBlocking<Unit> {
    Test().test()
}

Execution output result:

thread id: 15, thread name: DefaultDispatcher-worker-4 ---> end count: 58059

Not what we expected. Obviously, it is caused by data asynchrony during concurrency.

5.2.1 is volatile invalid?

Obviously, some people must also think that it can be solved by modifying variables with volatile. Is that really the case? It's not. We still can't get the desired result by modifying the count variable with volatile.
volatile guarantees visibility in concurrency, but not atomicity. count + + this operation, including read and write operations, is not an atomic operation. In this case, the desired results will not be obtained naturally.

5.2.2 using thread safe data structures

One solution is to use thread safe data structures. You can use the AtomicInteger class with incrementAndGet atomic operations:

class Test {
    private var count = AtomicInteger()
    suspend fun test() = withContext(Dispatchers.IO) {
        repeat(100) {
            launch {
                repeat(1000) {
                    count.incrementAndGet()
                }
            }
        }
        launch {
            delay(3000)
            printWithThreadInfo("end count: ${count.get()}")
        }
    }
}

fun main() = runBlocking<Unit> {
    Test().test()
}

Output results:

thread id: 35, thread name: DefaultDispatcher-worker-24 ---> end count: 100000

5.2.3 synchronous operation

Synchronize the increase of data. Code blocks with self incrementing count can be synchronized:

class Test {

    private val obj = Any()

    private var count = 0
    suspend fun test() = withContext(Dispatchers.IO) {
        repeat(100) {
            launch {
                repeat(1000) {
                    synchronized(obj) {  // Synchronous code block
                        count++
                    }
                }
            }
        }
        launch {
            delay(3000)
            printWithThreadInfo("end count: $count")
        }
    }
}

Or use ReentrantLock.

class Test {

    private val mLock = ReentrantLock()

    private var count = 0
    suspend fun test() = withContext(Dispatchers.IO) {
        repeat(100) {
            launch {
                repeat(1000) {
                    mLock.lock()
                    try{
                        count++
                    } finally {
                        mLock.unlock()
                    }
                }
            }
        }
        launch {
            delay(3000)
            printWithThreadInfo("end count: $count")
        }
    }
}

fun main() = runBlocking<Unit> {
    val cos = measureTimeMillis {
        Test().test()
    }
    printWithThreadInfo("cos time: ${cos.toString()}")
}

The output result is:

thread id: 60, thread name: DefaultDispatcher-worker-49 ---> end count: 100000
thread id: 1, thread name: main ---> cos time: 3127

The alternative in the collaboration process is called Mutex. It has lock and unlock methods. The key difference is that Mutex Lock () is a suspend function that does not block the current thread. There is also the withLock extension function, which can easily replace the commonly used Mutex lock(); , try {...} finally {Mutex. Unlock()} mode:

class Test {

    private val mutex = Mutex()

    private var count = 0
    suspend fun test() = withContext(Dispatchers.IO) {
        repeat(100) {
            launch {
                repeat(1000) {
                    mutex.withLock {
                        count++
                    }
                }
            }
        }
        launch {
            delay(3000)
            printWithThreadInfo("end count: $count")
        }
    }
}

5.2.4 limiting threads

If the count is self incremented in the same thread, there will be no data synchronization problem. Switch to a single thread each time the auto increment operation is performed. Like Android, UI refresh must switch to the main thread.

class Test {

    private val countContext = newSingleThreadContext("CountContext")

    private var count = 0
    suspend fun test() = withContext(countContext) {
        repeat(100) {
            launch {
                repeat(1000) {
                    count++
                }
            }
        }
        launch {
            delay(3000)
            printWithThreadInfo("end count: $count")
        }
    }
}

5.2.5 using Actors

An actor is an entity composed of a collaborative process, a state that is limited and encapsulated in the collaborative process, and a channel that communicates with other collaborative processes. A simple actor can be simply written as a function, but an actor with complex states is more suitable to be represented by classes.

There is an actor collaboration builder, which can easily combine the mailbox channel of the actor into its scope (used to receive messages) and send channel and result set objects, so that a single reference to the actor can be held as its handle.

The first step in using an actor is to define a message class to be processed by the actor. Kotlin's sealed class is well suited for this scenario. We use the IncCounter message (used to increment the counter) and the GetCounter message (used to get the value) to define the CounterMsg seal class. The latter needs to send a reply. The completable deferred communication primitive represents a single value that can be known (communicated) in the future, which is used here for this purpose.

// Various types of counter actors
sealed class CounterMsg
object IncCounter : CounterMsg() // One way message to increment counter
class GetCounter(val response: CompletableDeferred<Int>) : CounterMsg() // Request with reply

Next, define a function to start an actor using the actor collaboration Builder:

// This function starts a new counter actor
fun CoroutineScope.counterActor() = actor {
 var counter = 0 // actor status
 for (msg in channel) { // Iterator for upcoming messages
 when (msg) {
 is IncCounter -> counter++
 is GetCounter -> msg.response.complete(counter)
 }
 }
}

Main code:

class Test {

    suspend fun test() = withContext(Dispatchers.IO) {
        val counterActor = counterActor() // Create the actor
        repeat(100) {
            launch {
                repeat(1000) {
                    counterActor.send(IncCounter)
                }
            }
        }
        launch {
            delay(3000)
            // Send a message to get the count value from an actor
            val response = CompletableDeferred<Int>()
            counterActor.send(GetCounter(response))
            println("Counter = ${response.await()}")
            counterActor.close() // Close the actor
        }
    }
}

The correctness of the actor itself is irrelevant. An actor is a cooperative process, and a cooperative process is executed sequentially. Therefore, limiting the state to a specific cooperative process can solve the problem of sharing variable states. In fact, actors can modify their private state, but they can only interact with each other through messages (avoiding any locking).
An actor is more effective than a lock under high load because it always has work to do and does not need to switch to a different context at all.

In fact, coroutinescope The actor () method returns a SendChannel object. Channel is also part of the Kotlin collaboration process. The following articles will introduce it in detail.

Topics: Python Java Android kotlin computer