Android Kotlin's Coroutine (coroutine) detailed explanation

Coroutines are a concurrency design pattern that you can use on the Android platform to simplify code that executes asynchronously.
On Android, coroutines help manage long-running tasks that, if not managed properly, can block the main thread and render your app unresponsive.

Advantages of coroutines:

  • Lightweight
    You can run multiple coroutines on a single thread, since coroutines support suspension, which does not block the thread that is running the coroutine. Suspending saves memory compared to blocking and supports multiple parallel operations.
  • Fewer memory leaks
    Use structured concurrency to execute multiple operations in one scope
  • Built-in cancellation support
    Cancellation is automatically propagated throughout the running coroutine hierarchy.
  • Jetpack Integration
    Many of the Jetpack libraries include extensions that provide full coroutine support. ViewModel, etc. also provide coroutine scope (for example, viewModelScope) for structured concurrency


Coroutines mainly solve problems:

  • 1. Handle time-consuming tasks.
  • 2. Ensure main thread safety.

Coroutines can be similar to RxJava to achieve synchronization of asynchronous tasks and avoid possible callback hell. At the same time, the function identified by the suspend keyword is called a suspend function. This function can only be executed in a coroutine or other suspend functions, thereby ensuring the safety of the main thread.

There are many benefits of coroutines mentioned above. Next, let's write a simple coroutine first.

class CoroutineTestActivity : AppCompatActivity(){

		//也可以通过上面代理的方式实现(当然也可以直接用lifecycleScope)
    private val scope = MainScope()

    private val textContent by lazy {
        findViewById<TextView>(R.id.tv_corout_name)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_corout_one)
		//通过MainScope,启动一个协程
        scope.launch(Dispatchers.Main) {
			//异步数据同步化,获取数据
            val data = obtainCacheData()
			//数据放到UI上
            textContent.text = data
        }

	}
	private suspend fun obtainCacheData(): String {
		//通过Dispatchers调度器,切换到子线程执行耗时操作
        return withContext(Dispatchers.IO) {
            delay(8000)
            "缓存数据"
        }
    }
}



The code execution process:

  • Execute the onCreate function
  • Create a new coroutine on the main thread via MainScope.launch, and then the coroutine starts executing.
  • Inside a coroutine, calling obtainCacheData() now suspends further operations of the coroutine. Until the withContext block in obtainCacheData(), the execution ends
  • WithContext block, after execution, the coroutine created by launch in onCreate resumes execution and returns data.

Here you can see that compared with the general function, the coroutine has two more states of suspend and recovery .

The difference between hanging and blocking is that after the code is blocked, it will stay here forever. If it is suspended, after the suspension point is suspended, the suspension point is saved, and the main thread continues to execute. When the suspension function is executed, the execution operation is resumed.

Suspend function , obtainCacheData() is modified by suspend, also known as suspend function

The suspending function can only be called within the body of the coroutine or other suspending functions

look at the picture below

insert image description here

This article sorts out the scope, startup, use and cancellation of coroutines, and finally, the exception handling order of coroutines

1. Structured Concurrency

Coroutines follow the principles of structured concurrency. This means that new coroutines can only be started in the coroutine scope (CoroutineScope), thus limiting the life cycle of coroutines. Thus avoiding the waste of resources (memory, CPU, etc.) caused by the loss of a coroutine

Advantage:

  • Cancel the specified coroutine task
  • Handle exception information

When creating a coroutine, a scope must be specified, and the scope can be canceled to track all coroutines created by it.

1.1, the scope builder of the coroutine

CoroutineScope跟runBlocking。

  • runBlocking
    regular function, blocking thread
  • CoroutineScope
    suspends the function without blocking the thread. If an internal coroutine fails, other coroutines will also be cancelled.
  • supervisorScope
    suspends functions without blocking threads. An internal coroutine fails and does not affect other coroutines

They will all wait for the inner coroutine body and all child coroutines to complete.

runBlocking blocks threads

fun testRunBlocking() {
        runBlocking {
            launch {
                delay(2000)
                Log.d("liu", "testRunBlocking 222222")
            }
        }
        Log.d("liu", " testRunBlocking 111111")
    }

//Results of the

testRunBlocking 222222
testRunBlocking 111111

CoroutineScope does not block threads

 fun testCoroutineScope() {
        runBlocking {
            coroutineScope {
                launch {
                    delay(2000)
                    Log.d("liu", "testCoroutineScope 222222")
                }
                Log.d("liu", "testCoroutineScope 111111")
            }

        }
    }

...
//打印结果

testCoroutineScope 111111
testCoroutineScope 222222

CoroutineScope. If one of the internal coroutines fails, the other coroutines will also be cancelled.

    fun testCoroutineScope2() {
        runBlocking {
            coroutineScope {
                launch {
                    delay(2000)
                    Log.d("liu", "testCoroutineScope2 222222")
                }
                launch {
                    delay(1000)
                    Log.d("liu", "testCoroutineScope2 1111111")
                    val a = 2
                    val b = 0
                    val c = a / b

                }
            }
        }
    }

//Results of the

testCoroutineScope2 1111111

It can be seen here that the first started coroutine job1 did not print
coroutineScope. One coroutine failed, and other coroutines were also cancelled.


supervisorScope, if an internal coroutine fails, it will not affect other coroutines

fun testCoroutineScope3() {
        runBlocking {
            supervisorScope {
                launch {
                    delay(2000)
                   Log.d("liu", "testCoroutineScope3 222222")
                }
				//这里换成了async。跟异常机制有关,方便测试
                async {
                    delay(1000)
                   Log.d("liu", "testCoroutineScope3 111111")
                    val a = 2
                    val b = 0
                    //这里会出现除数为0异常
                    val c = a / b

                }
            }
        }
    }

//Results of the

testCoroutineScope3 111111
testCoroutineScope3 222222

It can be seen that after async has an exception, the first launch coroutine continues to execute.

1.2, Coroutine scope

Next, look at the commonly used coroutine scope (CoroutineScope)

  • GlobalScope
  • MainScope
  • ViewModelScope
  • LifecycleScope

1.2.1,GlobalScope

Global scope. When the Activity/Fragment is destroyed, the internal coroutine still exists.

1.2.2,MainScope

Used in Activity, can be canceled in onDestroy

code example

class CoroutineTestActivity : AppCompatActivity(){
     private val scope = MainScope()
     
    private val textContent by lazy {
        findViewById<TextView>(R.id.tv_corout_name)
    }
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_corout_one)
        test1()
    }
    private fun test1() {
        scope.launch(Dispatchers.Main) {
            val data = obtainCacheData()
            textContent.text = data
        }
    }
   override fun onDestroy() {
        super.onDestroy()
        scope.cancel()
    }
}

1.2.3,ViewModelScope

Used in ViewModel, bind the life cycle of ViewModel
Each ViewModel in the application defines ViewModelScope. If the ViewModel is cleared, coroutines started within this scope are automatically canceled. Coroutines are useful when you have work that needs to be done only while the ViewModel is active. For example, if you want to calculate some data for the layout, you should scope the work to the ViewModel so that when the ViewModel is cleared, the system automatically cancels the work to avoid consuming resources.

You can access the ViewModel's CoroutineScope through the ViewModel's viewModelScope property, as the following example shows:

class MyViewModel: ViewModel() {
    init {
        viewModelScope.launch {
            // Coroutine that will be canceled when the ViewModel is cleared.
        }
    }
}

1.2.4,LifecycleScope

Used in Activty/Fragment. Binding Activity/Fragment life cycle
Each Lifecycle object defines LifecycleScope. Coroutines started within this scope are canceled when the Lifecycle is destroyed. You can access the Lifecycle's CoroutineScope through the lifecycle.coroutineScope or lifecycleOwner.lifecycleScope properties.

The following example demonstrates how to use lifecycleOwner.lifecycleScope to create precomputed text asynchronously:

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

Restartable lifecycle-aware coroutines . When Lifecycle is in STARTED state, process data

class MyFragment : Fragment() {

    val viewModel: MyViewModel by viewModel()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        // Create a new coroutine in the lifecycleScope
        viewLifecycleOwner.lifecycleScope.launch {
            // repeatOnLifecycle launches the block in a new coroutine every time the
            // lifecycle is in the STARTED state (or above) and cancels it when it's STOPPED.
            viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
                // Trigger the flow and start listening for values.
                // This happens when lifecycle is STARTED and stops
                // collecting when the lifecycle is STOPPED
                viewModel.someDataFlow.collect {
                    // Process item
                }
            }
        }
    }
}

Click for details to restart the lifecycle-aware coroutine

2. The use of coroutines

In the first example, we started the coroutine through launch, let's take a look at its source code

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

You can see here that when starting the coroutine, there will be several optional parameters. If we don’t pass them, there will be default values.

Below, we will explain in detail by creating a coroutine and several parameters it needs (CoroutineContext, CoroutineStart, etc.).

2.1, coroutine builder

The coroutine is mainly through launch and async , these two commonly used construction methods.

2.1.1, start the coroutine through launch

The coroutine started by launch returns a Job. and returns no results

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

For multiple coroutines started by lauch, you can use the join() function to let the coroutines execute in order

Look at the code below

   private fun testJoin() {
        val job1 = launch {
            delay(1000)
            Log.d("liu","----第 1 个任务")
        }

        val job2 = launch {
            delay(2000)
            Log.d("liu","----第 2 个任务")
        }

        Log.d("liu","----第 3个任务")
    }

//打印结果

//----第 3 个任务
//----第 1 个任务
//----第 2 个任务

If we want the tasks to be executed in the order of 1, 2, 3 , we need to use join

code show as below

Look at the code below

   private fun testJoin2() {
        launch {
            val job1 =launch {
                delay(1000)
                Log.d("liu", "----第 1 个任务")
            }
            job1.join()
            val job2 = launch {
                delay(2000)
                Log.d("liu", "----第 2 个任务")
            }
            job2.join()

			Log.d("liu", "----第 3个任务")
        }
    }

//打印结果

//----第 1 个任务
//----第 2 个任务
//----第 3 个任务

Finally, look at the join() function

   public suspend fun join()

The join() function is also a suspend function and will not block.

2.1.2, start the coroutine through async

The coroutine started by async returns Deffered, a subclass of Job. The final return result can be obtained through the await() method

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

...

public interface Deferred<out T> : Job

get return result

    private fun testAsync() {
        launch {
            val job = async {
                delay(2000)
                "获取耗时数据"
            }
            Log.d("liu", "result: ${job.await()}")
        }
    }
//打印结果
 result: 获取耗时数据

As seen above, the result of async starting the coroutine is obtained through await

If async also wants to serialize (execute in order), you need to use the await() method

 private fun test4() {
        launch {

            val asyncJob1 = async {
                delay(1000)
                Log.d("liu", "----先执行的任务")
                "第 1 个任务完成"
            }
            asyncJob1.await()
            val asyncJob2 = async {
                delay(2000)
                Log.d("liu", "----后执行的任务")
                "第 2 个任务完成"
            }
    }

	...

//打印结果
----先执行的任务
----后执行的任务

Many times, we need to call multiple interfaces according to the return value, and then call other interfaces
insert image description here

Implemented using async.

 private fun test2() {
        launch {
            //开始并行执行

            val asyncJob1 = async {
                delay(2000)
                Log.d("liu","----第 1 个任务")
                "第 1 个任务完成"
            }
            val asyncJob2 = async {
                delay(2000)
                Log.d("liu","----第 2 个任务")
                "第 2 个任务完成"
            }
            //这样就是并行执行,不要在上面,添加await(),否则,就成了串行执行
			//async启动的协程,可以获取返回结果
            val resultJob1 = asyncJob1.await()
            val resultJob2 = asyncJob2.await()


            withContext(Dispatchers.IO){
                Log.d("liu","$resultJob1,$resultJob2,执行完成了----执行最后的任务")
            }

        }
    }

	...

//打印结果

----第 1 个任务
----第 2 个任务
第 1 个任务完成,第 2 个任务完成,执行完成了----执行最后的任务

This function, the final print result is *-the first task, the second task, the execution is completed----execute the last task*

Through the above, we can see that the coroutine started by async, calling the await() function, can get the result of time-consuming data.

Test two await() time-consuming situations

  /**
     * 测试耗时
     */
    fun measureTime() {
        launch {
            val time = measureTimeMillis {
                val asyncJob1 = async {
                    delay(2000)
                    Log.d("liu", "----第 1 个任务")
                    "第 1 个任务完成"
                }

                val asyncJob2 = async {
                    delay(1000)
                    Log.d("liu", "----第 2 个任务")
                    "第 2 个任务完成"
                }
                asyncJob1.await()
                asyncJob2.await()
            }
            Log.d("liu", "--measureTime耗时情况: $time")
        }
    }

    fun measureTime2() {
        launch {
            val time = measureTimeMillis {
                async {
                    delay(2000)
                    Log.d("liu", "----第 1 个任务")
                    "第 1 个任务完成"
                }.await()
                async {
                    delay(1000)
                    Log.d("liu", "----第 2 个任务")
                    "第 2 个任务完成"
                }.await()
            }
            Log.d("liu", "--measureTime2耗时情况: $time")
        }
    }

//结果
--measureTime耗时情况: 2180
--measureTime2耗时情况: 3178

From the above results, it can be seen that the timing of await() execution is very important.
The measureTime() method is the measureTime2() method executed in parallel
, which is executed serially.

Finally look at the await() function

    public suspend fun await(): T

The await() function is also a suspending function and will not block

Most of the time, if we perform time-consuming serial operations, we will use WithContext

 return withContext(Dispatchers.IO) {
            delay(8000)
            "缓存数据"
        }

Just like the code you started with, asynchronous code becomes synchronous. Also, it doesn't need to start a new coroutine

2.2, scheduler for coroutines

There are three main types of schedulers (similar to RxJava thread switching (Schedulers.newThread(), etc.)?)

  • Dispatchers.Default
  • Dispatchers.Main
  • Dispatchers.IO

Dispatchers.Default

Non-main thread, the default scheduler
is mainly for processing: CPU-intensive operations (data analysis, data calculation, etc.)

Dispatchers.Main

main thread

Mainly processing: UI update operation, call suspend suspend function

Dispatchers.IO

non-main thread

Mainly processing: IO operations (caches, files, databases, etc.) and network data

Dispatchers.Unconfined,newSingleThreadContext(“MyOwnThread”)

For details, see: Official Documentation

2.3, the start mode of the coroutine (CoroutineStart)

CoroutineStart is defined in the start option of the coroutine generator.

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

The pattern of CoroutineStart:

  • CoroutineStart.DEFAULT
  • CoroutineStart.ATOMIC
  • CoroutineStart.LAZY
  • CoroutineStart.UNDISPATCHED

official document

start introduction

CoroutineStart.DEFAULT

Scheduling begins immediately after the coroutine is created.
If the coroutine is canceled before it is scheduled, it will not execute, but will end with an exception being thrown.

look at the code

    fun coroutineStartOne() {
        runBlocking {
			//启动协程
            val job = launch(start = CoroutineStart.DEFAULT) {
                Log.d("liu", "default  start")
                delay(3000)
                Log.d("liu", "default  end")
            }
            delay(1000)
			//取消
            job.cancel()
        }
    }

As you can see here, the coroutine is canceled directly.

CoroutineStart.ATOMIC

After the coroutine is created, according to the context of the coroutine, start scheduling immediately.
The coroutine cannot be canceled until it reaches the first suspension point (suspend function)


    fun coroutineStartATOMIC() {
        runBlocking {
            val job = launch(start = CoroutineStart.DEFAULT) {
                //TODO: 这里做一些耗时操作,完成之前,不会被取消
                delay(3000)
                Log.d("liu", "ATOMIC  end")
            }
            job.cancel()
        }
    }


CoroutineStart.LAZY

It starts only when the coroutine is needed, including when the coroutine's start()/join()/await() and other functions are actively called. If the coroutine is canceled before
being executed, it will not execute, but will end with an exception being thrown.


     fun coroutineStartLAZY() {
        runBlocking {
            val job = launch(start = CoroutineStart.LAZY) {
                Log.d("liu", "LAZY  start")
            }
            Log.d("liu", "开始一些计算")
            delay(3000)
            Log.d("liu", "耗时操作完成")
            job.start()
        }
    }

//打印结果
开始一些计算
耗时操作完成
LAZY  start

Here, you can see that it will only be printed after calling start().

CoroutineStart.UNDISPATCHED

After the coroutine is created, it is executed immediately on the current function call stack (in which thread it is created and in which thread it is executed). Which thread is executed in which function is created? As can be seen from the name, it does not accept the scheduling of the thread specified by Dispatchers
until it encounters the first suspension point, which is similar to Dispatchers.Unconfined. Similar to
ATOMIC, even if it is canceled, it will execute to the first suspension point?

 fun coroutineStartUNDISPATCHED() {
        runBlocking {
            val job = async(context = Dispatchers.IO, start = CoroutineStart.UNDISPATCHED) {
                Log.d("liu", "线程IO:${Thread.currentThread().name}")
            }
            val job1 = async(context = Dispatchers.IO, start = CoroutineStart.DEFAULT) {
                Log.d("liu", "线程:${Thread.currentThread().name}")
            }
        }
    }

...
//打印结果
线程IO:main
线程:DefaultDispatcher-worker-1

It can be seen here. After changing the startup mode to DEFAULT, the thread also becomes an IO thread. UNDISPATCHED will only run on the thread specified in the creation of the coroutine

These modes feature:

  • 1. After DEFAULT and ATOMIC are created, they will be scheduled immediately (not executed immediately); LAZY will be executed only when triggered; UNDISPATCHED will be executed immediately
  • 2. The thread executed by UNDISPATCHED is the thread of the function that created it, even if the thread is specified, it is invalid
  • 3. When DEFAULT is canceled, it will be canceled immediately

2.4, the context of the coroutine (CoroutineContext)

A CoroutineContext defines the behavior of a coroutine using the following set of elements:

  • Job: Control the life cycle of the coroutine.
  • CoroutineDispatcher: Dispatches work to the appropriate thread.
  • CoroutineName: The name of the coroutine, which can be used for debugging.
  • CoroutineExceptionHandler: Handles uncaught exceptions.

For new coroutines created within the scope, the system will assign a new Job instance (not inherited) to the new coroutine, and other CoroutineContext elements (will be inherited). Inherited elements can be replaced by passing a new CoroutineContext to the launch or async function. Note that passing a Job to launch or async has no effect because the system always allocates a new instance of Job to the new coroutine.

  • The following code shows how to use the combination
    /**
     * 协程上下文的组合使用
     */
    private suspend fun testCoroutineContext() {
        launch(Job() + Dispatchers.Default + CoroutineName("coroutine new Name")) {
            println(" Thread info: ${Thread.currentThread().name}")
        }
    }

Here we see the context, which is connected by the "+" sign. If you are not unfamiliar with Kotlin, we know that it should rewrite the "+" operator. Let's take a look.

public interface CoroutineContext {
	...

	public operator fun plus(context: CoroutineContext): CoroutineContext =
}

  • Show the inheritance relationship of the coroutine context
private suspend fun testCoroutineContext2() {

        val exceptionHandler = CoroutineExceptionHandler { _, exception ->
            Log.d("liu", "exception: $exception")
        }
        Log.d("liu", "Top Job exceptionHandler: $exceptionHandler")
        //创建Job
        val topJob = Job()
        //创建一个新的协程作用域
        val scope = CoroutineScope(topJob + Dispatchers.Default + CoroutineName("coroutine new Name") + exceptionHandler)
        //打印顶层Job
        println("Top Job Info: $topJob")
        val job = scope.launch() {
            //打印协程相关信息
            Log.d("liu", "Job Info: ${coroutineContext[Job]}  ${coroutineContext[CoroutineName]} ${coroutineContext[CoroutineExceptionHandler]} , Thread info: ${Thread.currentThread().name}")
            val job2 = async {
                Log.d("liu", "Job Info: ${coroutineContext[Job]} ${coroutineContext[CoroutineName]} ${coroutineContext[CoroutineExceptionHandler]} , Thread info: ${Thread.currentThread().name}")
            }
            job2.await()
        }
        job.join()
    }

//打印结果
//异常Handler
Top Job exceptionHandler: cn.edu.coroutine.CoroutineTestActivity$testCoroutineContext2$$inlined$CoroutineExceptionHandler$1@6c29d69

//Job名字
Top Job Info: JobImpl{Active}@50933ee

//launch打印 job  协程名字  exceptionHandler  线程信息
Job Info: StandaloneCoroutine{Active}@ad1d08f  CoroutineName(coroutine new Name) cn.edu.coroutine.CoroutineTestActivity$testCoroutineContext2$$inlined$CoroutineExceptionHandler$1@6c29d69 , Thread info: DefaultDispatcher-worker-2

//launch打印 job  协程名字  exceptionHandler  线程信息
Job Info: DeferredCoroutine{Active}@67b8f1c CoroutineName(coroutine new Name) cn.edu.coroutine.CoroutineTestActivity$testCoroutineContext2$$inlined$CoroutineExceptionHandler$1@6c29d69 , Thread info: DefaultDispatcher-worker-1

Here, it can be clearly seen that the exceptionHandler is the same (ExceptionHandler$1@6c29d69), the jobs are all different, the Name is the same (coroutine new Name), and the thread information is all IO.

From here, you can see that for coroutines started by launch or async, the system will always assign new instances of Job to new coroutines. Therefore, Job will not be passed to sub-coroutines, but other attributes can be inherited

Transitive relationship: CoroutineScope—launch—async

The inheritance relationship of the coroutine context: 1. First, use the parameters passed in by itself; 2. Use the inherited parameters; 3. Use the default parameters

2.5, Job life cycle

The status of the job is:

  • Create (New)
  • Active
  • Completing / in progress (Completing)
  • Canceling
  • Cancelled
  • Completed

Official address: click here

The status of the Job has been introduced above, although it is impossible to directly judge these statuses. However, we can judge the status of the coroutine according to its properties isActive , isCompleted , and isCancelled .

The following is a diagram of the relationship between Job status and attributes (isActive, isCompleted, isCancelled)

State 【 isActive 】 【 isCompleted 】 【 isCancelled 】
Create (New) false false false
Active true false false
Completing true false false
Canceling false false true
Cancelled false true true
Completed false true false

For example, if the coroutine is completed normally, isCompleted=true & isCancelled=false;

Typically, a Job is active (created and started) when it is created. However, when the startup mode of the coroutine construction is CoroutineStart.LAZY, the state of the coroutine is in the creation state (New), and the Job is in the active (Active) state by calling join or start

official map

                                          wait children
    +-----+ start  +--------+ complete   +-------------+  finish  +-----------+
    | New | -----> | Active | ---------> | Completing  | -------> | Completed |
    +-----+        +--------+            +-------------+          +-----------+
                     |  cancel / fail       |
                     |     +----------------+
                     |     |
                     V     V
                 +------------+                           finish  +-----------+
                 | Cancelling | --------------------------------> | Cancelled |
                 +------------+                                   +-----------+


2.6, cancel the coroutine

The main contents of canceling the coroutine are:

  • Cancel the scope of the coroutine
  • Cancel the coroutine
    The cancellation of a single coroutine will not affect the rest of the sibling coroutines
    • Cancellation of CPU-intensive tasks
      • isActive
      • ensureAction
      • yield
  • Cancel the exception thrown by the coroutine

2.6.1, cancel the scope of the coroutine

Canceling the scope of a coroutine will cancel its child coroutines

fun coroutineScopeCancel() {
        //等待子协程执行完
        runBlocking<Unit> {
            //CoroutineScope不会继承runBlocking的属性。需要delay或者join
            val scope = CoroutineScope(Dispatchers.Default)
            scope.launch {
                delay(1000)
                Log.d("liu","启动 job 1")
            }
            scope.launch {
                delay(1000)
                Log.d("liu","启动 job 2")
            }
            //需要挂起,等待scope执行完
            delay(2000)
        }
    }

If no delay is added, the scope will not execute

 fun coroutineScopeCancel() {
        //等待子协程执行完
        runBlocking<Unit> {
            //CoroutineScope不会继承runBlocking的属性。需要delay或者join
            val scope = CoroutineScope(Dispatchers.Default)
            scope.launch {
                delay(1000)
                Log.d("liu","启动 job 1")
            }
            scope.launch {
                delay(1000)
                Log.d("liu","启动 job 2")
            }
            //需要挂起,等待scope执行完
            delay(200)
            scope.cancel()
            delay(2000)
        }
    }


In this way, nothing will be printed, and it can be seen that all its sub-coroutines have been canceled through the scope

2.6.2, cancel the coroutine

After the task of the coroutine is canceled, it will not affect the tasks of the brother coroutine

Normal coroutine cancellation

    fun coroutineCancel1() {
        runBlocking {
            val job1 = launch {
                delay(1000)
                Log.d("liu","启动 job 1")
            }
            val job2 = launch {
                delay(1000)
                Log.d("liu","启动 job 2")
            }
            job1.cancel()
        }
    }

...
//打印结果

启动 job 2

It can be seen here. After job1 is canceled, job2 will still execute

Cancellation of CPU-intensive (computational) tasks

There are 2 ways to cancel the task of calculating the code.
The first way is to periodically call a suspend function that checks for cancellation (via the ensureActive() function with the help of the Job lifecycle).
The other is to explicitly check whether the Job is in a canceled state through the yield() function.

Next, let's look at

  • With the help of Job life cycle processing

CPU-intensive tasks (with a large amount of calculation) cannot be canceled in time through cancel. At this time, we can help us cancel the calculation through the assistance of the Job life cycle

the case

We print the Log log every second. Then, cancel midway

look at the code

 fun coroutineCancelCPUIntensiveTask1() {
        runBlocking {
            val startTime = System.currentTimeMillis()
            val job = launch {
                var nextTime = startTime
                var totalPrintCount = 0
                while (totalPrintCount < 5) {
                    if (System.currentTimeMillis() >= nextTime) {
                        nextTime = 1000
                        totalPrintCount++
                        Log.d("liu", "+++日志打印: $totalPrintCount")
                    }
                }
            }

            delay(1000)
            Log.d("liu", "开始执行取消")
            job.cancel()
            Log.d("liu", "执行取消完成")
        }
    }


Through the execution results, we can see that after the execution is canceled, the calculation is still completed before the cancellation

At this time, we know through the life cycle of the Job that after the cancel is executed, the state of the Job becomes Canceling.

insert image description here

The Job attribute isActive that we can access corresponding to Canceling becomes false.
insert image description here

In this way, in the while loop, we add isActive is being calculated (whether it is true) to assist us in the calculation, so that after the cancellation (Cancel), we can exit in time.

code example

fun coroutineCancelCPUIntensiveTask2() {
        runBlocking {
            val startTime = System.currentTimeMillis()
            val job = launch {
                var nextTime = startTime
                var totalPrintCount = 0
                while (totalPrintCount < 5 && isActive) {
                    if (System.currentTimeMillis() >= nextTime) {
                        nextTime = 1000
                        totalPrintCount++
                        Log.d("liu", "+++日志打印: $totalPrintCount")
                    }
                }
            }

            delay(1000)
            Log.d("liu", "开始执行取消")
            job.cancel()
            Log.d("liu", "执行取消完成")
        }
    }

insert image description here

  • Handled by ensureActive()

During the execution of the coroutine, we can also use the ensureActive() function to confirm whether the coroutine is active. If it is inactive, it will throw a coroutine-specific exception to cancel the coroutine.

sample code

fun coroutineCancelCPUIntensiveTask3() {
        runBlocking {
            val startTime = System.currentTimeMillis()
            val job = launch {
                var nextTime = startTime
                var totalPrintCount = 0
                while (totalPrintCount < 5 ) {
                    ensureActive()
                    if (System.currentTimeMillis() >= nextTime) {
                        nextTime = 1000
                        totalPrintCount++
                        Log.d("liu", "+++日志打印: $totalPrintCount")
                    }
                }
            }

            delay(1000)
            Log.d("liu", "开始执行取消")
            job.cancel()
            Log.d("liu", "执行取消完成")
        }
    }

This code is also cancelable. Similar to the code above. Cancel the isActive judgment and add ensureActive() judgment.
In fact, the ensureActive() function is also handled internally by throwing an exception, but it is silently processed. The exception handling of the coroutine will be described in detail later.

Look at the code of the ensureActive() function

public fun CoroutineScope.ensureActive(): Unit = coroutineContext.ensureActive()

Call the ensureActive() function of the coroutine context

public fun CoroutineContext.ensureActive() {
    get(Job)?.ensureActive()
}

The ensureActive() function of Job is called

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

getCancellationException(). Estimate is to get, optional exception when we Cancel()

public fun getCancellationException(): CancellationException

As you can see here, if it is not active, an exception CancellationException is thrown.

CancellationException is an optional parameter when we execute Cancel().

At this point, we know that when we want to cancel but cannot cancel the coroutine, we can also actively throw this exception to cancel, because of this exception, the coroutine will silently handle it (the above ordinary coroutine is canceled, it has been analyzed).

  • Handled by the yield() function

Official address: click

The yield() function will check the status of the coroutine (also by judging whether the job is active), and if it is inactive, an exception will be thrown to cancel the coroutine.

In addition, it will try to give up the execution right of the thread to provide execution opportunities for other coroutines.

First, let's look at the yield() function

public suspend fun yield(): Unit = suspendCoroutineUninterceptedOrReturn sc@ { uCont ->
    val context = uCont.context
    context.checkCompletion()
	....
}

keep watching

internal fun CoroutineContext.checkCompletion() {
    val job = get(Job)
    if (job != null && !job.isActive) throw job.getCancellationException()
}

Here, if the job does not exist or is in an inactive state (!isActive), a job.getCancellationException() exception is thrown. This function, when we looked at ensureActive(), we just saw that it was CancellationException.

Therefore, the yield() function will also check whether the coroutine is active, if not, throw an exception and cancel it.

code example

fun coroutineCancelCPUIntensiveTaskYield() {
        runBlocking {
            val startTime = System.currentTimeMillis()
            val job = launch {
                var nextTime = startTime
                var totalPrintCount = 0
                while (totalPrintCount < 5 ) {
                    yield()
                    if (System.currentTimeMillis() >= nextTime) {
                        nextTime = 1000
                        totalPrintCount++
                        Log.d("liu", "+++日志打印: $totalPrintCount")
                    }
                }
            }
            delay(1000)
            Log.d("liu", "开始执行取消")
            job.cancel()
            Log.d("liu", "执行取消完成")
        }
    }

When the amount of calculation is particularly large, try to use the yield() function

Cancel the exception thrown by the coroutine

The cancellation of the coroutine is canceled by throwing an exception (CancellationException).

When canceling, the exception we defined can be passed in. However, if there is no incoming word, why is no exception found?

This is handled internally by Kotlin.

Let's look at the cancel() method of Job

public interface Job : CoroutineContext.Element {
	...
	public fun cancel(cause: CancellationException? = null)
	...
}

It can be seen that when we cancel, it is an optional function

Let's catch the exception by try

  fun coroutineCancelException() {
        runBlocking {
            runBlocking {
                val job1 = launch {
                    try {
                        delay(1000)
                        Log.d("liu", "启动 job 1")
                    } catch (e: CancellationException) {
                        e.printStackTrace()
                    }
                }
                val job2 = launch {
                    delay(1000)
                    Log.d("liu", "启动 job 2")
                }
                job1.cancel()
            }
        }
    }

Let's customize another exception to see

fun coroutineCancelException() {
        runBlocking {
            runBlocking {
                val job1 = launch {
                    delay(1000)
                    Log.d("liu","启动 job 1")
                }
                val job2 = launch {
                    delay(1000)
                    Log.d("liu","启动 job 2")
                }
                job1.cancel(CancellationException("主动抛出异常"))
            }
        }
    }

print result

主动抛出异常

Our exception information actively throws an exception and is printed out

2.7, after canceling the coroutine, the resources are released

2.7.1, catching exceptions and releasing resources

When canceling the coroutine above, we said that the cancellation of the coroutine is achieved by throwing an exception.
We can use try...catch to catch this exception. Then, if we have resources that need to be released, we can also use try...catch...finally to release our resources in finally.

   fun coroutineCancelResourceRelease() {
        runBlocking {
            runBlocking {
                val job1 = launch {
                    try {
                        delay(1000)
                        Log.d("liu", "启动 job 1")
                    }finally {
                        Log.d("liu", "finally job 1")
                    }
                }
                val job2 = launch {
                    delay(1000)
                    Log.d("liu", "启动 job 2")
                }
                job1.cancel()
            }
        }
    }

Here, you can see that the code in finally is executed. If we want to release resources, we can try to suspend the point and release resources in finally.

2.7.2, release resources through the use() function

This function can only be used by objects that implement Closeable, and the close() method will be called automatically when the program ends, suitable for file objects

not use() function

    fun coroutineCancelResourceReleaseByUse() {
        runBlocking {
            val buffer = BufferedReader(FileReader("xxx"))
            with(buffer) {
                var line: String?
                try {
                    while (true) {
                        line = readLine() ?: break;
                        Log.d("liu", line)
                    }
                } finally {
                    close()
                }
            }
		}
	}

You need to close the resource yourself in finally

Use the use() function

    fun coroutineCancelResourceReleaseByUse() {
        runBlocking {
            BufferedReader(FileReader("xxx")).use {
                while (true) {
                    val line = readLine() ?: break;
                    Log.d("liu", line)
                }
            }
        }
    }

We don't need to close it ourselves. Look at the use function


public inline fun <T : Closeable?, R> T.use(block: (T) -> R): R {
    var exception: Throwable? = null
    try {
        return block(this)
		...
    } finally {
      		...
                try {
                    close()
                } catch (closeException: Throwable) {
                    // cause.addSuppressed(closeException) // ignored here
                }
			....
    }
}

This function has been implemented in finally, the function

2.7.3, NonCancellable - suspend function in cancel

A NonCancellable Job is always active. It is designed for the withContext() function. To prevent cancellation, a block of code that needs to be executed without cancellation.
For example, the failure of the coroutine task execution. Call the interface to notify the background and so on.

NonCancellable This object is not applicable to launch, async and other coroutine builders. If you use it in launch, then when the parent coroutine is canceled, not only the newly started child coroutine will not be canceled, but the relationship between the parent and the child will also be cut off. The parent does not wait for the child to finish executing, nor does it cancel if the child exceptions.

Official document: please click here

withContext(NonCancellable) {
    // 这里执行的代码块,不会被取消
}

In general, when canceling the coroutine, if we capture it through try...finally, we release the resource in finally.
If there is a suspending function in finally, then the function cannot be executed (the coroutine is in an inactive state)

The coroutine in the canceling state cannot use the suspend function. When the coroutine is canceled and needs to call the suspend function, it is necessary to make the suspend function active through NonCancellable, which will suspend the running code.

example sample code

fun coroutineUnCancelException() {
        runBlocking {
            runBlocking {
                val job1 = launch {
                    try {
                        repeat(1000) { i ->
                            delay(1000)
                            Log.d("liu", "启动 job 1,index: $i")
                        }
                    } catch (e: CancellationException) {
                        e.printStackTrace()
                    } finally {
                        delay(2000)
                        Log.d("liu", "finally job 1")
                    }
                }
                job1.cancel()
            }
        }
    }

When canceling, there is also a suspending function in finally (just like the success/failure of the task, it is the same as notifying the background)

At this time, the log in finally cannot be printed out.

We need to use NonCancellable

example

fun coroutineUnCancelException2() {
        runBlocking {
            runBlocking {
                val job1 = launch {
                    try {
                        repeat(1000) { i ->
                            delay(1000)
                            Log.d("liu", "启动 job 1,index: $i")
                        }
                    } finally {
                        //在cancel里,有挂起函数后,需要用到NonCancellable
                        withContext(NonCancellable){
                            delay(2000)
                            Log.d("liu", "finally job 1")
                        }

                    }
                }
                job1.cancel()
            }
        }
    }

In this case, the log in finally can be successfully printed. If we need to call suspending functions such as network requests when an exception occurs, it can be done in this way.

2.8, Cancellation of overtime tasks (withTimeout)

The most practical reason for canceling the execution of the coroutine is that its execution time exceeds, for example, network requests, etc.

Although we can track and cancel such tasks in other ways, we can directly use the withTimeout function to achieve the same effect

Official address: please click here

Examples are as follows:

withTimeout(1300L) {
    repeat(1000) { i ->
        println("I'm sleeping $i ...")
        delay(500L)
    }
}

print result

I'm sleeping 0 ...
I'm sleeping 1 ...
I'm sleeping 2 ...
Exception in thread "main" kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 1300 ms

Before, when we cancel the coroutine, an exception will be thrown, which will be handled silently.
Here, we can see that the timeout task will throw a TimeoutCancellationException exception, which is not silently processed.

However, sometimes, we don't want to throw an exception, we want to return Null or a default value. Then, we need to use withTimeoutOrNull .
Sample code:

fun coroutineTimeOutOrNull() {
        runBlocking {
            launch {
               val result =  withTimeoutOrNull(1300L) {
                    repeat(1000) { i ->
                        Log.d("liu", "I'm sleeping $i ...")
                        delay(500L)
                    }
                    "执行完成"
                } ?: "执行未完成,默认值"

                Log.d("liu","Result is $result")
            }

        }
    }

Here, we see that what is returned is a default value.

3. Coroutine exception handling

We know that the cancellation exception actually throws a CancellationException exception at the suspension point, and the mechanism of the coroutine will ignore/silently handle it.

Here, let's look at the handling mechanism if an exception occurs during the cancellation process of the coroutine or if multiple sub-coroutines of the same coroutine experience exceptions.

The main contents include:

  • Coroutine Exception Propagation
  • Automatically propagating exceptions vs. user exposing exceptions
  • Exception propagation for non-root coroutines
  • supervisorJob
  • Coroutine exception handling

3.1, the propagation of coroutine exceptions

According to the method of the coroutine builder, there are two ways of exceptions: automatic exception propagation (launch/actor) and exposing exceptions to users (async/produce).

When these constructors are used to create a root coroutine (not a child coroutine of another coroutine), the former constructors (launch/actor) treat exceptions as uncaught exceptions, similar to Java's Thread.uncaughtExceptionHandler; while the latter ( async /produce) rely on the user to finally handle these exceptions.

Official address: stamp

3.1.1, exception propagation of the root coroutine (automatic propagation of exceptions and user public exceptions)

The above are all exception propagation methods with coroutines, let’s take a look

/**
     * 根协程异常的传播
     */
    private fun handleException() {
        runBlocking {
            val job = GlobalScope.launch {
                try {
                    Log.d("liu", "launch 抛出未捕获异常")
                    throw IndexOutOfBoundsException()
                } catch (e: java.lang.IndexOutOfBoundsException) {
                    Log.d("liu", "launch 捕获 IndexOutOfBoundsException")
                }

            }
            job.join()
            val deferred = GlobalScope.async {
                Log.d("liu", "async 抛出向用户公开的异常")
                throw ArithmeticException()
            }
            try {
                deferred.await()
            } catch (e: ArithmeticException) {
                Log.d("liu", "async 捕获 ArithmeticException")
            }
        }
    }

print result

launch 抛出未捕获异常
launch 捕获 IndexOutOfBoundsException
async 抛出向用户公开的异常
async 捕获 ArithmeticException

From the above code and print results, we can see that

For the coroutine started by launch, we catch the exception at the exception point (similar to java uncaught exception). For the coroutine
started by async, we catch the exception at await(), which is an exception that we want to expose to the user. If the await() function is not called, this exception will not be thrown.

3.1.2, exception propagation of non-root coroutines

Exceptions in non-root coroutines are always propagated

sample code

 fun handleExceptionByNoteRootCoroutine() {
        runBlocking {
            launch {
                async {
                    Log.d("liu", "async 抛出异常")
                    throw ArithmeticException()
                }
            }
        }
    }

After executing here, an exception will be thrown directly. It can be seen from here that if async is started through the root coroutine, no exception will be thrown without calling await(); if it is started through a non-root coroutine, no matter whether the await() function is adjusted or not, an exception will be thrown.

Here, the exception thrown by async will go directly to launch and throw an uncaught exception.

3.1.3, the propagation characteristics of exceptions

When a coroutine fails due to an exception, it propagates the exception to its parent. The parent will do the following:

  • Cancel its child coroutines
  • cancel itself
  • propagates the exception to its parent

If this is the case, if a child coroutine fails, all coroutines associated with it will fail, which is not what we expected.

If we want to break this propagation feature, we can use supervisorJob

3.1.4,supervisorJob,supervisorScope

Subroutines running inside a supervisorScope do not propagate exceptions to their parents. So, if we don't want the exception to propagate continuously to the parent, we can use supervisorScope or supervisorJob.

Official address: click

Compared to coroutineScope, we can use supervisorScope for concurrency. It only propagates cancellation in one direction, and cancels all children (passing down) only if it fails itself. It also waits for all children to finish like coroutineScope does.

Supervision job

val supervisor = SupervisorJob()
with(CoroutineScope(coroutineContext + supervisor)) {
    // launch the first child -- its exception is ignored for this example (don't do this in practice!)
    val firstChild = launch(CoroutineExceptionHandler { _, _ ->  }) {
        println("The first child is failing")
        throw AssertionError("The first child is cancelled")
    }
    // launch the second child
    val secondChild = launch {
        firstChild.join()
        // Cancellation of the first child is not propagated to the second child
        println("The first child is cancelled: ${firstChild.isCancelled}, but the second one is still active")
        try {
            delay(Long.MAX_VALUE)
        } finally {
            // But cancellation of the supervisor is propagated
            println("The second child is cancelled because the supervisor was cancelled")
        }
    }
    // wait until the first child fails & completes
    firstChild.join()
    println("Cancelling the supervisor")
    supervisor.cancel()
    secondChild.join()
}

print result

The first child is failing
The first child is cancelled: true, but the second one is still active
Cancelling the supervisor
The second child is cancelled because the supervisor was cancelled

Supervision scope

try {
    supervisorScope {
        val child = launch {
            try {
                println("The child is sleeping")
                delay(Long.MAX_VALUE)
            } finally {
                println("The child is cancelled")
            }
        }
        // Give our child a chance to execute and print using yield 
        yield()
        println("Throwing an exception from the scope")
        throw AssertionError()
    }
} catch(e: AssertionError) {
    println("Caught an assertion error")
}

print result

The child is sleeping
Throwing an exception from the scope
The child is cancelled
Caught an assertion error

The difference between a normal job and a supervisor job is exception handling. Each child should handle its exceptions by itself through the exception handling mechanism, with one difference: failures of the children are not propagated to the parent (passed down). This means that coroutines started directly within supervisorScope use the CoroutineExceptionHandler in the same scope as the root coroutine.

val handler = CoroutineExceptionHandler { _, exception -> 
    println("CoroutineExceptionHandler got $exception") 
}
supervisorScope {
    val child = launch(handler) {
        println("The child throws an exception")
        throw AssertionError()
    }
    println("The scope is completing")
}
println("The scope is completed")


print result

The scope is completing
The child throws an exception
CoroutineExceptionHandler got java.lang.AssertionError
The scope is completed

Prevent APP flashback, CoroutineExceptionHandler for abnormal handling of coroutines

Optional element in coroutine context, used for exception catching. The exception capture of the coroutine uses CoroutineExceptionHandler for exception capture.

CoroutineExceptionHandler is only called on uncaught exceptions (those that are not handled in any other way). In particular, all child coroutines (coroutines created in the context of another Job) delegate the handling of their exceptions to their parent coroutines, which in turn delegate to their parent coroutines, and so on up to the root, so that a CoroutineExceptionHandler installed in a child coroutine context is never used. Besides that, coroutines built via async always catch all exceptions and represent them in the generated Deferred object, so it doesn't work using CoroutineExceptionHandler.

We know that exceptions can be caught through CoroutineExceptionHandler. However, not all exceptions can be caught, and the following conditions need to be met:

  • The exception is thrown by a coroutine that automatically throws an exception (launch starts, not async)
  • in a CoroutineContext of a CoroutineScope or a direct child of the root coroutine (CoroutineScope or supervisorScope)

code example


fun test() {
        val handler = CoroutineExceptionHandler { _, exception ->
            Log.d("liu", "CoroutineExceptionHandler got $exception")
        }
        runBlocking {
            val job1 = GlobalScope.launch(handler) {
                throw NullPointerException()
            }
			//通过async构建的协程无效
            val job2 = GlobalScope.async(handler) {
                throw IndexOutOfBoundsException()
            }
            joinAll(job1, job2)
        }
    }

print result

CoroutineExceptionHandler got java.lang.NullPointerException

Caused by: java.lang.IndexOutOfBoundsException
        at cn.edu.coroutine.CoroutineExceptionHandleActivity$test$1$job2$1.invokeSuspend(CoroutineExceptionHandleActivity.kt:83)

It can be seen here that the coroutine started by launch is captured, but the one started by async is not captured.

Look at the example below. The coroutine started by MainScope in Activity

fun test2() {
        val handler = CoroutineExceptionHandler { _, exception ->
            Log.d("liu", "test2 got $exception")
        }
        //是否是根协程
        //是否是supervisorJob
        launch(handler) {
            launch {
                throw NullPointerException()
            }
        }
        launch {
            launch(handler) {
                throw IllegalStateException()
            }
        }
    }

print result

test2 got java.lang.NullPointerException

 java.lang.IllegalStateException
        at cn.edu.coroutine.CoroutineExceptionHandleActivity$test2$2$1.invokeSuspend(CoroutineExceptionHandleActivity.kt:105)


After we set the Handler with the coroutine, the NullPointerException was caught. The Handler set in the sub-coroutine did not catch the IllegalStateException exception.
Like the system built-in MainScope, LifecycleScope, and ViewModelScope all use SupervisorJob

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

...

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

abnormal aggregation

When multiple sub-coroutines of a coroutine fail due to exceptions, generally the first exception is taken for processing. All other exceptions that occur after the first exception will be bound to the first exception.

look at the code


 fun multiException() {
        val handler = CoroutineExceptionHandler { _, exception ->
            Log.d("liu", "test3 got $exception  ,${exception.suppressed.contentToString()}")
        }
        val scope = CoroutineScope(Job())
        val job = scope.launch(handler) {
            launch {
                try {
                    delay(Long.MAX_VALUE)
                } finally {
                    throw IllegalArgumentException()
                }
            }
            launch {
                delay(100)
                throw NullPointerException()
            }
        }
    }

print result

test3 got java.lang.NullPointerException  ,[java.lang.IllegalArgumentException]

Code flow: When the second child coroutine is executed, a NullPointerException exception will be thrown; then the exception will be passed to the parent, and the parent will cancel all the children, and the first child coroutine will also be canceled. When cancelling, an IllegalArgumentException will be thrown. Catch and print exceptions through CoroutineExceptionHandler.

Here you can see that exception prints the first caught exception, and other exceptions will be stored in the suppressed array.

Android coroutine official document
Kotlin coroutine official website

Guess you like

Origin blog.csdn.net/ecliujianbo/article/details/128190986