Kotlin's coordination process starts and cancels the coordination process

Posted by jcornett on Wed, 19 Jan 2022 12:59:40 +0100

Builder of collaborative process

Both the launch and async builders are used to start new processes
launch, which returns a job without any result value
async returns a Deferred, which is also a job and can be used await() gets its final result on a Deferred value

    //Wait for a job: join and await
    private fun runBlocking1(){
        //runBlocking can turn the main thread into a coroutine
        //job1 and job2 are subprocesses of runBlocking
        //runBlocking will wait for the execution of the two subprocesses job1 and job2 to complete, blocking the main thread (blocking: the button will not pop up immediately after being pressed, and will pop up only after the execution of job1 and job2)
        runBlocking {

            val job1 = launch {
                delay(2000)
                Log.v("zx", "job1 to finish")
            }

            val job2 = async {
                delay(2000)
                Log.v("zx", "job2 to finish")
                "job2 value"
            }
            //await can get the return value
            val job2Result = job2.await()
            Log.v("zx", "job2 Return value of: $job2Result")

        }

Requirement: wait for job1 to be executed before executing job2 and job3
If you start it through launch, use the join function
If you start with async, use the await function

        
        //join and await are both suspended functions that do not block the main thread

        //If you start it through launch, use the join function
        runBlocking {

            val job1 = launch {
                delay(2000)
                Log.v("zx", "job1 to finish")
            }
            //This function will wait for job1 to execute before executing the following functions
            job1.join()

            val job2 = launch {
                delay(100)
                Log.v("zx", "job2 to finish")
            }
            val job3 = launch {
                delay(100)
                Log.v("zx", "job3 to finish")
            }

        }
        //If you start with async, use the await function
        runBlocking {

            val job1 = async {
                delay(2000)
                Log.v("zx", "job1 to finish2")
            }
            //This function will wait for job1 to execute before executing the following functions
            job1.await()

            val job2 = async {
                delay(100)
                Log.v("zx", "job2 to finish2")
            }
            val job3 = async {
                delay(100)
                Log.v("zx", "job3 to finish2")
            }

        }
    }

Requirement: the result of the addition of the first two tasks is given to the third task (async structured concurrency)

   //runBlocking in the main thread, the child will inherit the context of the parent
   //runBlocking is dispatchers When started in main, doOne and doTwo will also use the scheduler dispatchers of the parent collaboration Start in main
    private fun runBlocking2() {
        //The result of the addition of the first two tasks is given to the third task (async structured concurrency)
        runBlocking {
            val time = measureTimeMillis {
                //Synchronous
                val one = doOne()
                val two = doTwo()
                Log.v("zx", "data ${one + two}")
            }
            Log.v("zx", "time = $time")
        }
        runBlocking {
            val time = measureTimeMillis {
                //Asynchronous
                val one = async { doOne() }
                val two = async { doTwo() }
                Log.v("zx", "data ${one.await() + two.await()}")
                
                //The following is wrong
                //val one2 = async { doOne() }.await()
                //val two2 = async { doTwo() }.await()
                //Log.v("zx", "data ${one2+two2}")
            }
            Log.v("zx", "asynctime = $time")
        }
    }


    private suspend fun doOne():Int{
        delay(1000)
        return 1
    }
    private suspend fun doTwo():Int{
        delay(1000)
        return 2
    }

Four startup modes of collaborative process

CoroutineStart.DEFAULT: start scheduling immediately after the collaboration is created. If the collaboration is cancelled before scheduling, cancel will be executed
CoroutineStart.ATOMIC: scheduling starts immediately after the collaboration is created. The collaboration will not respond to cancellation until it reaches the first hanging point
CoroutineStart.LAZY: the coordination process will start scheduling only when it is needed, including actively calling the start, join, await and other functions of the coordination process. If it is cancelled before scheduling, the coordination process will enter the abnormal end state
CoroutineStart.UNDISPATCHED: the coroutine is executed in the current function stack immediately after it is created until the first real hang point is encountered

    private fun runBlocking3(){
        //runBlocking will wait for all subprocesses to be executed
        runBlocking {
            val job1 = launch(start = CoroutineStart.DEFAULT) {
                delay(3000)
                Log.v("zx","finished")
            }
            delay(1000)
            //CoroutineStart.DEFAULT will be cancelled
            job1.cancel()

            val job11 = launch(start = CoroutineStart.ATOMIC) {
                //Delay is the first hang function, and delay here is the first hang starting point,
                // If the cancellation is not performed before the first hang point, ATOMIC will not respond to the cancellation
                delay(3000)
                Log.v("zx","finished")
            }
            delay(1000)
            job11.cancel()


            val job2 = async(start = CoroutineStart.LAZY) {
                20
            }
            delay(2000)
            //If it is cancelled before scheduling, it will enter an abnormal state
            job2.cancel()
            //If it is launch, start it with join. If it is async, start it with start or await
            Log.v("zx","job2 ${job2.await()}")

            //How to use dispatchers IO, is your collaboration still in the main thread?
            //Answer: use coroutinestart Undispatched because the current function runBlocking is on the main thread

            //DISPATCHED means forwarding, and UN DISPATCHED means not forwarding (the collaboration created in the main thread is executed in the main thread)
            //UNDISPATCHED is immediate execution, while others are immediate scheduling. Immediate scheduling does not mean immediate execution
            //Immediately execute in the current function stack, which is in the main thread
            val job3 = async(context = Dispatchers.IO, start = CoroutineStart.UNDISPATCHED) {
                Log.v("zx","current ${Thread.currentThread().name}")
            }
        }

    }

Scope builder for a collaboration

coroutineScope and runBlocking

The difference is
runBlocking is a regular function, while coroutineScope is a suspended function. They all wait for the execution of the subprocess to end
runBlocking blocks the current thread to wait
coroutineScope is only suspended and will release the underlying thread for other purposes

coroutineScope: if a collaboration fails, all other sibling collaborations will be cancelled
supervisorScope: if one collaboration fails, it will not affect other sibling collaborations

coroutineScope: if a collaboration fails, all other sibling collaborations will be cancelled

    private fun runBlocking4(){
        //Structured concurrency, coroutinescope (scope builder)

        runBlocking{
            //The coroutine scope must wait for the execution of the two sub processes job1 and job2,
            //coroutineScope inherits the scope of the parent collaboration
            coroutineScope {
                val job1 = launch {
                    delay(500)
                    Log.v("zx", "job1 to finish")
                }

                val job2 = async {
                    delay(100)
                    Log.v("zx", "job2 to finish")
                    "job2 value"
                    throw NullPointerException()
                }
            }
        }
    }

supervisorScope: if one collaboration fails, it will not affect other sibling collaborations

    private fun runBlocking4(){
        //Structured concurrency, coroutinescope (scope builder)

        runBlocking{
            //The coroutine scope must wait for the execution of the two sub processes job1 and job2,
            //coroutineScope inherits the scope of the parent collaboration
            supervisorScope {
                val job1 = launch {
                    delay(500)
                    Log.v("zx", "job1 to finish")
                }

                val job2 = async {
                    delay(100)
                    Log.v("zx", "job2 to finish")
                    "job2 value"
                    throw NullPointerException()
                }
            }
        }
    }

Job object

  • Each created collaboration (through launch or async) will return a job instance, which is the unique identification of the collaboration and is responsible for managing the lifecycle of the collaboration
  • A task can contain a series of statuses: new, active, completing, completed, canceling, and cancelled. Although we cannot directly access these statuses, we can access the job properties, isActive, iscancelled, and isCompleted

job lifecycle

If the coroutine is active, the coroutine runs incorrectly or invokes job Cancel() will set the current task as being cancelled (isactive = false, isCanceled = true). When all sub processes are completed, the process will enter the cancelled state (isCanceled = true), and isCompleted = true

Cancellation of collaboration

  • Canceling a scope cancels its children
  • The cancelled subprocess will not affect other sibling subprocesses
  • The coroutine handles the cancellation operation by throwing cancelationexception
  • All kotlinx The hang functions (withcontext, delay, etc.) in coroutines are cancelable
               runBlocking {
            //CoroutineScope builds a collaboration scope by itself and does not inherit the context of the runBlocking parent collaboration
            val scope = CoroutineScope(Dispatchers.Default)
            val job1 = scope.launch {
                try {
                    delay(1000)
                    Log.v("zx", "job1")
                } catch (e: Exception) {
                    e.printStackTrace()
                }


            }
            val job2 = scope.launch {
                delay(1000)
                Log.v("zx", "job2")

            }

            delay(100)
            //If you cancel the scope here, the child collaboration will be cancelled
            //The cancelled subprocess will not affect other brother subprocesses, so job2 prints it out
            //job1.cancel()
            //Custom cancel exception
            job1.cancel(CancellationException("I cancelled"))
            //Here, it will be printed first. runBlocking will not wait for the execution of sub processes in CoroutineScope to complete
            Log.v("zx", "runBlocking")
        }
        
Print:
com.z.zjetpack V/zx: runBlocking
com.z.zjetpack W/System.err: java.util.concurrent.CancellationException: I cancelled
com.z.zjetpack V/zx: job2

CPU intensive task cancellation

isActive is an extension property in CoroutineScope that can be used to check whether a job is active
ensureActive(): this method will throw an exception immediately if the job is inactive
The yield function will check the status of the collaboration. If it has been cancelled, it will throw a cancelationexception in response. It will also try to give up thread execution rights and provide execution opportunities for other processes. (if this task preempts system resources, you can use yield)

The following are intensive tasks that do not contain suspended functions
             runBlocking {
            val startTime = System.currentTimeMillis()
            val job1 = launch(Dispatchers.Default) {
                var nextPrintTime = startTime
                var i = 0
                while (i < 5) {
                    //Print every 0.5 seconds
                    if (System.currentTimeMillis() > nextPrintTime) {
                        Log.v("zx", "i = ${i++}")
                        nextPrintTime += 500
                    }

                }
            }
           
            Log.v("zx", "Waiting for cancellation")
            delay(1000)
            //Cannot cancel because there is no pending function for suspend keyword
            //job1.cancel()
            //job1.join()
            //It is equivalent to the above two methods. Why do you use join? Join means waiting. After executing the cancel() method, you will not cancel immediately, but enter canceling,
            //That is, it is being cancelled, so the join method is to wait for the cancellation to be completed.
            job1.cancelAndJoin()
            Log.v("zx", "Canceling")
        }

Print:
com.z.zjetpack V/zx: Waiting for cancellation
com.z.zjetpack V/zx: i = 0
com.z.zjetpack V/zx: i = 1
com.z.zjetpack V/zx: i = 2
com.z.zjetpack V/zx: i = 3
com.z.zjetpack V/zx: i = 4
com.z.zjetpack V/zx: Cancelled

It can be found that we call cancelAndJoin to cancel, and the final result is that it is not cancelled. How can we cancel this intensive task?

while (i < 5 && isActive) 
while (i < 5) {
  ensureActive()
  ...
while (i < 5) {
   yield()
   ...
Print:
com.z.zjetpack V/zx: Waiting for cancellation
com.z.zjetpack V/zx: i = 0
com.z.zjetpack V/zx: i = 1
com.z.zjetpack V/zx: i = 2
com.z.zjetpack V/zx: Cancelled

The above three methods can be cancelled.

Side effects of collaborative process cancellation

  • Release resources in finally
  • use function: this function can only be used by objects that implement Closeable. The close method will be called automatically at the end of the program, which is suitable for file objects.

Because an exception will be thrown when the collaboration is cancelled, the following code will not be executed. The following code may release resources. If the following code is not executed, it will not release resources, such as IO operations. What should be done?
A: in the finally release mode, the finally code block will be executed regardless of whether it is taken or cancelled

        runBlocking {
           val job1=  launch {
               try {
                   repeat(10) {
                       Log.v("zx","sleep")
                       delay(1000)
                   }
               }finally {
                   //It will be executed here whether it is taken or not
                   //After canceling, an exception will be thrown, which will not affect me to release resources
                   Log.v("zx","Release resources")
               }

            }
            delay(2000)
            job1.cancel()
        }
use function, for example, we need to read txt file

Common writing:

    private fun read(){
        val input = assets.open("1.txt")
        val br = BufferedReader(InputStreamReader(input))
        with(br) {
            var line: String?
            try {
                while (true) {
                    line = readLine() ?: break
                    Log.v("zx","data $line")
                }
            } finally {
                close()
            }
        }
    }

use writing

    private fun readUse(){
        val input = assets.open("1.txt")
        val br = BufferedReader(InputStreamReader(input))
        with(br) {
            var line: String?
            use {
                while (true) {
                    line = readLine() ?: break
                    Log.v("zx","data $line")
                }
            }
        }
    }

Tasks that cannot be cancelled

  • A collaboration in cancellation cannot be suspended. If you want to call the suspend function after the collaboration is cancelled, you need to put it in withContext(NonCancellable). In this way, the running code will be suspended and remain in the canceling state until the task processing is completed.

example:

        runBlocking {
            val job1 = launch {
                try {
                    repeat(10) {
                        Log.v("zx", "sleep")
                        delay(1000)
                    }
                } finally {
                    Log.v("zx", "start sleep")
                    delay(1000)
                    Log.v("zx", "end sleep")
                }


            }
            delay(2000)
            job1.cancel()
        }
Print:
com.z.zjetpack V/zx: sleep
com.z.zjetpack V/zx: sleep
com.z.zjetpack V/zx: start sleep

It can be found that the end sleep will never print out. What should I do?
Use withContext(NonCancellable)

        runBlocking {
            val job1 = launch {
                try {
                    repeat(10) {
                        Log.v("zx", "sleep")
                        delay(1000)
                    }
                } finally {
                    //If you want the cancellation of the coroutine not to affect the call of the pending function here, you need to use withContext(NonCancellable) 
                        // This can also be used for long-term missions
                    withContext(NonCancellable) {
                        Log.v("zx", "start sleep")
                        delay(1000)
                        Log.v("zx", "end sleep")
                    }

                }


            }
            delay(2000)
            job1.cancel()
        }
Print:
com.z.zjetpack V/zx: sleep
com.z.zjetpack V/zx: sleep
com.z.zjetpack V/zx: start sleep
com.z.zjetpack V/zx: end sleep

Timeout task

  • Many requests to cancel a collaboration are made because it may time out
  • withTimeoutOrNull performs a timeout operation by returning null instead of throwing an exception
    example
  runBlocking {
           //It needs to be processed within 1 second
            withTimeout(1000) {
                repeat(10) {
                    Log.v("zx", "sleep")
                    delay(500)
                }
            }
        }

As above, if it is not processed within one second, an exception kotlinx will be thrown coroutines. TimeoutCancellationException: Timed out waiting for 1000 ms

So what if the network request doesn't return within 1 second, we don't want to throw an exception, but just want to return a default value?
So what if we don't want to throw an exception and just want to return a null value?
Answer: use withTimeoutOrNull

  runBlocking {
            //It is processed within 1 second. If it is not processed within 1 second, null will be returned instead of throwing an exception
            val result = withTimeoutOrNull(1000) {
                repeat(10) {
                    Log.v("zx", "sleep")
                    delay(500)
                }
                "complete"
            } ?: "Default data"

            Log.v("zx", "result: $result")
        }
Print:
com.z.zjetpack V/zx: sleep
com.z.zjetpack V/zx: sleep
com.z.zjetpack V/zx: Results: default data

As above, if it is completed within 1 second, the result is completed. If it is not completed, the result will be null. If it is null, the default data will be displayed

finish

Topics: Android kotlin jetpack coroutine