[关闭]
@TryLoveCatch 2024-03-23T02:18:20.000000Z 字数 17800 阅读 185

Kotlin知识体系之协程

Kotlin知识体系


重要概念

协程是什么?

协程是一个基于线程的上层框架!
它和Executor、AsyncTask一样,都是对 Thread API 的封装,让我们可以在写代码时,不用关注多线程就能够很方便地写出并发操作。

挂起是什么?

挂起,就是一个稍后会被自动切回来的线程调度操作。
挂起的对象是协程。

挂起的全名应该叫非阻塞式挂起,其意思是为不会阻塞其他协程,只是当前自己所在协程会挂起等待不执行,但是其他协程还是能继续执行的。
挂起是针对其他协程的,如果只有一个协程,那就是和阻塞的效果一样了

挂起的非阻塞式是怎么回事?

挂起的非阻塞式指的是它能用看起来阻塞的代码写出非阻塞的操作。

协程创建

runBlocking

顶层函数,创建一个新的协程同时阻塞当前线程,直到其内部所有逻辑以及子协程所有逻辑全部执行完成,返回值是泛型T,一般在项目中不会使用,主要是为main函数和测试设计的。

定义如下:

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

看一个例子:

  1. fun runBloTest() {
  2. "1111: ${Thread.currentThread().name}".loge("TryLoveCatch")
  3. //创建一个全局作用域协程,不会阻塞当前线程,生命周期与应用程序一致
  4. runBlocking {
  5. "2222: ${Thread.currentThread().name}".loge("TryLoveCatch")
  6. //在这1000毫秒内该协程所处的线程不会阻塞
  7. //协程将线程的执行权交出去,该线程继续干它要干的事情,到时间后会恢复至此继续向下执行
  8. delay(1000)//1秒无阻塞延迟(默认单位为毫秒)
  9. "3333: ${Thread.currentThread().name}".loge("TryLoveCatch")
  10. }
  11. "4444: ${Thread.currentThread().name}".loge("TryLoveCatch")
  12. }

结果如下:

  1. 2023-06-03 16:45:44.479 21446-21446/TryLoveCatch: 1111: main
  2. 2023-06-03 16:45:44.482 21446-21446/TryLoveCatch: 2222: main
  3. // 1S后
  4. 2023-06-03 16:45:45.485 21446-21446/TryLoveCatch: 3333: main
  5. 2023-06-03 16:45:45.487 21446-21446/TryLoveCatch: 4444: main

只有在runBlocking协程体逻辑全部运行结束后,声明在runBlocking之后的代码才能执行,即runBlocking会阻塞其所在线程。

runBlocking本身是在当前线程,不会新开始一个线程。虽然会阻塞当前线程的,但其内部运行的协程又是非阻塞的。

launch

launch是最常用的用于启动协程的方式,用于在不阻塞当前线程的情况下启动一个协程,并返回对该协程任务的引用,即Job对象。

定义如下:

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

在非协程环境中launch有两种方式创建协程:

GlobalScope.launch()

在应用范围内启动一个新协程,不会阻塞调用线程,协程的生命周期与应用程序一致。
表示一个不绑定任何Job的全局作用域,用于启动顶层协程,这些协程在整个应用程序生命周期中运行,不会提前取消(不存在Job)。

看一个例子:

  1. fun launchTest() {
  2. "1111: ${Thread.currentThread().name}".loge("TryLoveCatch")
  3. //创建一个全局作用域协程,不会阻塞当前线程,生命周期与应用程序一致
  4. GlobalScope.launch {
  5. "2222: ${Thread.currentThread().name}".loge("TryLoveCatch")
  6. //在这1000毫秒内该协程所处的线程不会阻塞
  7. //协程将线程的执行权交出去,该线程继续干它要干的事情,到时间后会恢复至此继续向下执行
  8. delay(1000)//1秒无阻塞延迟(默认单位为毫秒)
  9. "3333: ${Thread.currentThread().name}".loge("TryLoveCatch")
  10. }
  11. "4444: ${Thread.currentThread().name}".loge("TryLoveCatch")
  12. }

结果如下:

  1. 2023-06-03 16:41:59.808 19810-19810/TryLoveCatch: 1111: main
  2. 2023-06-03 16:41:59.814 19810-19810/TryLoveCatch: 4444: main
  3. 2023-06-03 16:41:59.815 19810-19857/TryLoveCatch: 2222: DefaultDispatcher-worker-1
  4. // 1S后
  5. 2023-06-03 16:42:00.821 19810-19857/TryLoveCatch: 3333: DefaultDispatcher-worker-1

GlobalScope.launch会新起一个线程,可以看出来是一个线程池。

CoroutineScope.launch()

启动一个新的协程而不阻塞当前线程,并返回对协程的引用作为一个Job。
通过CoroutineContext至少一个协程上下文参数创建一个 CoroutineScope对象。
协程上下文控制协程生命周期和线程调度,使得协程和该组件生命周期绑定,组件销毁时,协程一并销毁,从而实现安全可靠地协程调用。
这是在应用中最推荐使用的协程使用方式。

看一个例子:

  1. fun launchTest() {
  2. "1111: ${Thread.currentThread().name}".loge("TryLoveCatch")
  3. //开启一个IO模式的协程,通过协程上下文创建一个CoroutineScope对象,需要一个类型为CoroutineContext的参数
  4. val job = CoroutineScope(Dispatchers.IO).launch {
  5. "2222: ${Thread.currentThread().name}".loge("TryLoveCatch")
  6. delay(1000)//1秒无阻塞延迟(默认单位为毫秒)
  7. "3333: ${Thread.currentThread().name}".loge("TryLoveCatch")
  8. }
  9. "4444: ${Thread.currentThread().name}".loge("TryLoveCatch")
  10. }

结果如下:

  1. 2023-06-03 16:52:21.651 23565-23565/TryLoveCatch: 1111: main
  2. 2023-06-03 16:52:21.664 23565-23565/TryLoveCatch: 4444: main
  3. 2023-06-03 16:52:21.666 23565-23634/TryLoveCatch: 2222: DefaultDispatcher-worker-1
  4. 2023-06-03 16:52:22.670 23565-23634/TryLoveCatch: 3333: DefaultDispatcher-worker-1

协程环境中使用launch

通过launch在一个协程中启动子协程。

看一个例子:

  1. private fun launchTest() {
  2. "1111: ${Thread.currentThread().name}".loge("TryLoveCatch")
  3. GlobalScope.launch {
  4. "2222: ${Thread.currentThread().name}".loge("TryLoveCatch")
  5. delay(1000)//1秒无阻塞延迟(默认单位为毫秒)
  6. "3333: ${Thread.currentThread().name}".loge("TryLoveCatch")
  7. //在协程内创建子协程
  8. launch {
  9. "4444: ${Thread.currentThread().name}".loge("TryLoveCatch")
  10. delay(1500)//1.5秒无阻塞延迟(默认单位为毫秒)
  11. "5555: ${Thread.currentThread().name}".loge("TryLoveCatch")
  12. }
  13. "6666: ${Thread.currentThread().name}".loge("TryLoveCatch")
  14. }
  15. "7777: ${Thread.currentThread().name}".loge("TryLoveCatch")
  16. }

结果如下:

  1. 2023-06-03 17:00:33.751 26510-26510/TryLoveCatch: 1111: main
  2. 2023-06-03 17:00:33.758 26510-26510/TryLoveCatch: 7777: main
  3. 2023-06-03 17:00:33.758 26510-26557/TryLoveCatch: 2222: DefaultDispatcher-worker-1
  4. // 1S后
  5. 2023-06-03 17:00:34.761 26510-26557/TryLoveCatch: 3333: DefaultDispatcher-worker-1
  6. 2023-06-03 17:00:34.762 26510-26557/TryLoveCatch: 6666: DefaultDispatcher-worker-1
  7. 2023-06-03 17:00:34.763 26510-26556/TryLoveCatch: 4444: DefaultDispatcher-worker-2
  8. // 1.5S后
  9. 2023-06-03 17:00:36.264 26510-26557/TryLoveCatch: 5555: DefaultDispatcher-worker-1

有没有注意到:4444和5555不是一个线程了,这个是为什么呢?我也不清楚。。。

async

async类似于launch,都是创建一个不会阻塞当前线程的新的协程。它们区别在于:async的返回是Deferred对象,可通过Deffer.await()等待协程执行完成并获取结果。

定义如下:

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

看一个例子:

  1. private fun asyncTest() {
  2. "${Thread.currentThread().name} -> start".loge("TryLoveCatch")
  3. GlobalScope.launch {
  4. val time = measureTimeMillis {//计算执行时间
  5. val deferredOne: Deferred<Int> = async {
  6. delay(2000)
  7. "${Thread.currentThread().name} -> asyncOne".loge("TryLoveCatch")
  8. 100//这里返回值为100
  9. }
  10. val deferredTwo: Deferred<Int> = async {
  11. delay(3000)
  12. "${Thread.currentThread().name} -> asyncTwo".loge("TryLoveCatch")
  13. 200//这里返回值为200
  14. }
  15. val deferredThr: Deferred<Int> = async {
  16. delay(4000)
  17. "${Thread.currentThread().name} -> asyncThr".loge("TryLoveCatch")
  18. 300//这里返回值为300
  19. }
  20. //等待所有需要结果的协程完成获取执行结果
  21. val result = deferredOne.await() + deferredTwo.await() + deferredThr.await()
  22. "${Thread.currentThread().name} -> result == $result".loge("TryLoveCatch")
  23. }
  24. "${Thread.currentThread().name} -> 耗时 $time ms".loge("TryLoveCatch")
  25. }
  26. "${Thread.currentThread().name} -> end".loge("TryLoveCatch")
  27. }

运行结果如下:

  1. 2023-06-05 13:38:41.235 30000-30000/TryLoveCatch: main -> start
  2. 2023-06-05 13:38:41.240 30000-30000/TryLoveCatch: main -> end
  3. 2023-06-05 13:38:43.244 30000-30054/TryLoveCatch: DefaultDispatcher-worker-1 -> asyncOne
  4. 2023-06-05 13:38:44.244 30000-30054/TryLoveCatch: DefaultDispatcher-worker-1 -> asyncTwo
  5. 2023-06-05 13:38:45.245 30000-30054/TryLoveCatch: DefaultDispatcher-worker-1 -> asyncThr
  6. 2023-06-05 13:38:45.246 30000-30054/TryLoveCatch: DefaultDispatcher-worker-1 -> result == 600
  7. 2023-06-05 13:38:45.247 30000-30054/TryLoveCatch: DefaultDispatcher-worker-1 -> 耗时 4006 ms

使用async来创建协程,特别需要注意的是:

Job & Deferred

Job

Job是launch构建协程返回的一个协程任务,完成时是没有返回值的。可以把Job看成协程对象本身,封装了协程中需要执行的代码逻辑,协程的操作方法都在Job身上。Job具有生命周期并且可以取消,它也是上下文元素,继承自CoroutineContext。

Job几个比较重要的函数:

  1. public interface Job : CoroutineContext.Element {
  2. //活跃的,是否仍在执行
  3. public val isActive: Boolean
  4. //启动协程,如果启动了协程,则为true;如果协程已经启动或完成,则为false
  5. public fun start(): Boolean
  6. //取消Job,可通过传入Exception说明具体原因
  7. public fun cancel(cause: CancellationException? = null)
  8. //挂起协程直到此Job完成
  9. public suspend fun join()
  10. //取消任务并等待任务完成,结合了[cancel]和[join]的调用
  11. public suspend fun Job.cancelAndJoin()
  12. //给Job设置一个完成通知,当Job执行完成的时候会同步执行这个函数
  13. public fun invokeOnCompletion(handler: CompletionHandler): DisposableHandle
  14. }

可以看出来,Job跟与Thread很像:

看一个例子,模拟一个无限循环的协程,当协程是活跃状态时每秒钟打印两次消息,1.2秒后取消协程:

  1. private fun jobTest() = runBlocking {
  2. val startTime = System.currentTimeMillis()
  3. // Dispatchers.Default:默认调度器,非主线程。CPU密集型任务调度器,适合处理后台计算。
  4. val job = launch(Dispatchers.Default){
  5. "${Thread.currentThread().name} -> job start".loge("TryLoveCatch")
  6. var nextPrintTime = startTime
  7. var i = 0
  8. while (isActive) {//当job是活跃状态继续执行
  9. if (System.currentTimeMillis() >= nextPrintTime) {//每秒钟打印两次消息
  10. "${Thread.currentThread().name} -> job: I'm sleeping ${i++} ...".loge("TryLoveCatch")
  11. nextPrintTime += 500
  12. }
  13. }
  14. }
  15. "${Thread.currentThread().name} -> 111111".loge("TryLoveCatch")
  16. delay(2000)//延迟2s
  17. "${Thread.currentThread().name} -> 等待2秒后".loge("TryLoveCatch")
  18. //job.join()
  19. //job.cancel()
  20. job.cancelAndJoin()//取消任务并等待任务完成
  21. "${Thread.currentThread().name} -> 协程被取消并等待完成".loge("TryLoveCatch")
  22. }

运行结果如下:

  1. 2023-06-05 14:17:47.144 13813-13813/TryLoveCatch: main -> 111111
  2. 2023-06-05 14:17:47.144 13813-13852/TryLoveCatch: DefaultDispatcher-worker-1 -> job start
  3. 2023-06-05 14:17:47.146 13813-13852/TryLoveCatch: DefaultDispatcher-worker-1 -> job: I'm sleeping 0 ...
  4. 2023-06-05 14:17:47.639 13813-13852/TryLoveCatch: DefaultDispatcher-worker-1 -> job: I'm sleeping 1 ...
  5. 2023-06-05 14:17:48.139 13813-13852/TryLoveCatch: DefaultDispatcher-worker-1 -> job: I'm sleeping 2 ...
  6. 2023-06-05 14:17:48.638 13813-13852/TryLoveCatch: DefaultDispatcher-worker-1 -> job: I'm sleeping 3 ...
  7. 2023-06-05 14:17:49.139 13813-13852/TryLoveCatch: DefaultDispatcher-worker-1 -> job: I'm sleeping 4 ...
  8. 2023-06-05 14:17:49.147 13813-13813/TryLoveCatch: main -> 等待2秒后
  9. 2023-06-05 14:17:49.149 13813-13813/TryLoveCatch: main -> 协程被取消并等待完成

join()是一个挂起函数,它需要等待协程的执行。

去掉delay(2000),结果如下:

  1. 2023-06-05 14:20:31.230 15044-15081/TryLoveCatch: DefaultDispatcher-worker-1 -> job start
  2. 2023-06-05 14:20:31.230 15044-15044/TryLoveCatch: main -> 111111
  3. 2023-06-05 14:20:31.231 15044-15081/TryLoveCatch: DefaultDispatcher-worker-1 -> job: I'm sleeping 0 ...
  4. 2023-06-05 14:20:31.237 15044-15044/TryLoveCatch: main -> 协程被取消并等待完成

如果去掉job.cancelAndJoin(),那么子协程会一直运行。

Deferred

Deferred继承自Job,具有与Job相同的状态机制。它是async构建协程返回的一个协程任务,可通过调用await()方法等待协程执行完成并获取结果。
不同的是Job没有结果值,Deffer有结果值。

  1. public interface Deferred<out T> : Job {
  2. //等待协程执行完成并获取结果
  3. public suspend fun await(): T
  4. }

在说async的时候,已经举过例子了,可以参考下。

作用域

协程作用域(CoroutineScope)其实就是为协程定义的作用范围,为了确保所有的协程都会被追踪,Kotlin 不允许在没有使用CoroutineScope的情况下启动新的协程。

协程作用域本质是一个接口:

  1. public interface CoroutineScope {
  2. //此域的上下文。Context被作用域封装,用于在作用域上扩展的协程构建器的实现。
  3. public val coroutineContext: CoroutineContext
  4. }

每个协程生成器launch、async等都是CoroutineScope的扩展。

因为启动协程需要作用域,但是作用域又是在协程创建过程中产生的,这似乎是一个“先有鸡后有蛋还是先有蛋后有鸡”的问题。

常用作用域

官方库给我们提供了一些作用域可以直接来使用:

  1. fun scopeTest() {
  2. //创建一个根协程
  3. GlobalScope.launch {//父协程
  4. launch {//子协程
  5. print("GlobalScope的子协程")
  6. }
  7. launch {//第二个子协程
  8. print("GlobalScope的第二个子协程")
  9. }
  10. }
  11. //为UI组件创建主作用域
  12. val mainScope = MainScope()
  13. mainScope.launch {//启动协程
  14. //todo
  15. }
  16. }

Jetpack的协程支持

Android 官方对协程的支持是非常友好的,KTX 为 Jetpack 的Lifecycle相关组件提供了已经绑定UV声明周期的作用域供我们直接使用:

在build.gradle添加Lifecycle相应基础组件后,再添加以下组件即可:

  1. // ViewModel
  2. implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0"
  3. // LiveData
  4. implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.2.0"
  5. // 只有Lifecycles(没有 ViewModel 和 LiveData)
  6. implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.2.0"

因为Activity 实现了LifecycleOwner这个接口,而lifecycleScope则正是它的拓展成员,可以在Activity中直接使用lifecycleScope协程实例:

  1. class MainActivity : AppCompatActivity() {
  2. override fun onCreate(savedInstanceState: Bundle?) {
  3. super.onCreate(savedInstanceState)
  4. setContentView(R.layout.activity_main)
  5. btn_data.setOnClickListener {
  6. lifecycleScope.launch {//使用lifecycleScope创建协程
  7. //协程执行体
  8. }
  9. }
  10. }
  11. }

在ViewModel中使用创建协程:

  1. class MainViewModel : ViewModel() {
  2. fun getData() {
  3. viewModelScope.launch {//使用viewModelScope创建协程
  4. //执行协程
  5. }
  6. }
  7. }

作用域划分及父子协程规则

官方框架在实现复合协程的过程中也提供了作用域,主要用于明确父子关系,以及取消或者异常处理等方面的传播行为。该作用域分为以下三种:

除了三种作用域中提到的行为以外,父子协程之间还存在以下规则:

调度器

调度器确定相应的协程使用那些线程来执行。

CoroutineDispatcher调度器指定指定执行协程的目标载体,它确定了相关的协程在哪个线程或哪些线程上执行。可以将协程限制在一个特定的线程执行,或将它分派到一个线程池,亦或是让它不受限地运行。

  1. public abstract class CoroutineDispatcher :
  2. AbstractCoroutineContextElement(ContinuationInterceptor), ContinuationInterceptor {
  3. //询问调度器是否需要分发
  4. public open fun isDispatchNeeded(context: CoroutineContext): Boolean = true
  5. //将可运行块的执行分派到给定上下文中的另一个线程上。这个方法应该保证给定的[block]最终会被调用。
  6. public abstract fun dispatch(context: CoroutineContext, block: Runnable)
  7. //返回一个continuation,它封装了提供的[continuation],拦截了所有的恢复。
  8. public final override fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T>
  9. //CoroutineDispatcher是一个协程上下文元素,而'+'是一个用于协程上下文的集合和操作符。
  10. public operator fun plus(other: CoroutineDispatcher): CoroutineDispatcher = other
  11. }

系统提供的调度器

Kotlin 提供了四个调度器,您可以使用它们来指定应在何处运行协程:

调度器 说明 场景
Dispatchers.Default 默认调度器,非主线程。CPU密集型任务调度器,适合处理后台计算。 通常处理一些单纯的计算任务,或者执行时间较短任务比如:Json的解析,数据计算等。
Dispatchers.Main UI调度器, Andorid 上的主线程。 调度程序是单线程的,通常用于UI交互,刷新等。
Dispatchers.Unconfined 一个不局限于任何特定线程的协程调度程序,即非受限调度器。 子协程切换线程代码会运行在原来的线程上,协程在相应的挂起函数使用的任何线程中继续。
Dispatchers.IO IO调度器,非主线程,执行的线程是IO线程。 适合执行IO相关操作,比如:网络处理,数据库操作,文件读写等。

withContext

官方为我们提供了一个withContext顶级函数,在获取数据函数内,调用withContext(Dispatchers.IO)来创建一个在IO线程池中运行的块。您放在该块内的任何代码都始终通过IO调度器执行。由于withContext本身就是一个suspend函数,它会使用协程来保证主线程安全。

  1. //用给定的协程上下文调用指定的挂起块,挂起直到它完成,并返回结果。
  2. public suspend fun <T> withContext(
  3. context: CoroutineContext,
  4. block: suspend CoroutineScope.() -> T
  5. ): T
  1. GlobalScope.launch(Dispatchers.Main) {//开始协程:主线程
  2. val result: User = withContext(Dispatchers.IO) {//网络请求(IO 线程)
  3. userApi.getUserSuspend("FollowExcellence")
  4. }
  5. tv_title.text = result.name //更新 UI(主线程)
  6. }

在主线程中启动一个协程,然后再通过withContext(Dispatchers.IO)调度到IO线程上去做网络请求,获取结果返回后,主线程上的协程就会恢复继续执行,完成UI的更新。
由于withContext可让在不引入回调的情况下控制任何代码行的线程池,因此可以将其应用于非常小的函数,如从数据库中读取数据或执行网络请求。一种不错的做法是使用withContext来确保每个函数都是主线程安全的,那么可以从主线程调用每个函数。调用方也就无需再考虑应该使用哪个线程来执行函数了。您可以使用外部 withContext来让 Kotlin 只切换一次线程,这样可以在多次调用的情况下,以尽可能避免了线程切换所带来的性能损失。

协程上下文

CoroutineContext表示协程上下文,是 Kotlin 协程的一个基本结构单元。
协程上下文主要承载着资源获取,配置管理等工作,是执行环境的通用数据资源的统一管理者。它有很多作用,包括携带参数,拦截协程执行等等。

如何运用协程上下文是至关重要的,以此来实现正确的线程行为、生命周期、异常以及调试。
协程使用以下几种元素集定义协程的行为,它们均继承自CoroutineContext:

协程上下文的数据结构与List和Map非常类似。它包含用户定义的一些数据集合,这些数据与协程密切相关。它是一个有索引的 Element 实例集合。每个 element 在这个集合有一个唯一的Key。

  1. //协程的持久上下文。它是[Element]实例的索引集,这个集合中的每个元素都有一个唯一的[Key]。
  2. public interface CoroutineContext {
  3. //从这个上下文中返回带有给定[key]的元素或null。
  4. public operator fun <E : Element> get(key: Key<E>): E?
  5. //从[initial]值开始累加该上下文的项,并从左到右应用[operation]到当前累加器值和该上下文的每个元素。
  6. public fun <R> fold(initial: R, operation: (R, Element) -> R): R
  7. //返回一个上下文,包含来自这个上下文的元素和来自其他[context]的元素。
  8. public operator fun plus(context: CoroutineContext): CoroutineContext
  9. //返回一个包含来自该上下文的元素的上下文,但不包含指定的[key]元素。
  10. public fun minusKey(key: Key<*>): CoroutineContext
  11. //[CoroutineContext]元素的键。[E]是带有这个键的元素类型。
  12. public interface Key<E : Element>
  13. //[CoroutineContext]的一个元素。协程上下文的一个元素本身就是一个单例上下文。
  14. public interface Element : CoroutineContext {
  15. //这个协程上下文元素的key
  16. public val key: Key<*>
  17. public override operator fun <E : Element> get(key: Key<E>): E?
  18. }
  19. }

Element本身也实现了CoroutineContext 接口。
注意:协程上下文的内部实现实际是一个单链表。

CoroutineName

CoroutineName是用户用来指定的协程名称的,用于方便调试和定位问题

  1. //用户指定的协程名称。此名称用于调试模式。
  2. public data class CoroutineName(
  3. //定义协程的名字
  4. val name: String
  5. ) : AbstractCoroutineContextElement(CoroutineName) {
  6. //CoroutineName实例在协程上下文中的key
  7. public companion object Key : CoroutineContext.Key<CoroutineName>
  8. }

举个例子:

  1. GlobalScope.launch(CoroutineName("GlobalScope")) {
  2. launch(CoroutineName("CoroutineA")) {//指定协程名称
  3. val coroutineName = coroutineContext[CoroutineName]//获取协程名称
  4. print(coroutineName)
  5. }
  6. }

协程内部可以通过coroutineContext这个全局属性直接获取当前协程的上下文。打印数据如下:

  1. [DefaultDispatcher-worker-2] CoroutineName(CoroutineA)

上下文组合

从上面的协程创建的函数中可以看到,协程上下文的参数只有一个,但是怎么传递多个上下文元素呢?CoroutineContext可以使用 " + " 运算符进行合并。由于CoroutineContext是由一组元素组成的,所以加号右侧的元素会覆盖加号左侧的元素,进而组成新创建的CoroutineContext。
感觉CoroutineContext跟Map真的很像!!!

看个例子:

  1. GlobalScope.launch {
  2. //通过+号运算添加多个上下文元素
  3. var context = CoroutineName("协程1") + Dispatchers.Main
  4. print("context == $context")
  5. context += Dispatchers.IO //添加重复Dispatchers元素,Dispatchers.IO 会替换 Dispatchers.Main
  6. print("context == $context")
  7. val contextResult = context.minusKey(context[CoroutineName]!!.key)//移除CoroutineName元素
  8. print("contextResult == $contextResult")
  9. }

注意:如果有重复的元素(key一致)则会右边的会代替左边的元素。打印数据如下:

  1. context == [CoroutineName(协程1), Dispatchers.Main]
  2. context == [CoroutineName(协程1), Dispatchers.IO]
  3. contextResult == Dispatchers.IO

启动模式

CoroutineStart是一个枚举类,为协程构建器定义启动选项。在协程构建的start参数中使用。

启动模式 含义 说明
DEFAULT 默认启动模式,立即根据它的上下文调度协程的执行 是立即调度,不是立即执行,DEFAULT 是饿汉式启动,launch 调用后,会立即进入待调度状态,一旦调度器 OK 就可以开始执行。如果协程在执行前被取消,其将直接进入取消响应的状态。
LAZY 懒启动模式,启动后并不会有任何调度行为,直到我们需要它执行的时候才会产生调度 包括主动调用该协程的start、join或者await等函数时才会开始调度,如果调度前就被取消,协程将直接进入异常结束状态。
ATOMIC 类似[DEFAULT],以一种不可取消的方式调度协程的执行 虽然是立即调度,但其将调度和执行两个步骤合二为一了,就像它的名字一样,其保证调度和执行是原子操作,因此协程也一定会执行。
UNDISPATCHED 类似[ATOMIC],立即执行协程,直到它在当前线程中的第一个挂起点。 是立即执行,因此协程一定会执行。即使协程已经被取消,它也会开始执行,但不同之处在于它在同一个线程中开始执行。

这些启动模式的设计主要是为了应对某些特殊的场景。业务开发实践中通常使用DEFAULT和LAZY这两个启动模式就够了。

suspend 挂起函数

suspend 是 Kotlin 协程最核心的关键字,使用suspend关键字修饰的函数叫作挂起函数,挂起函数只能在协程体内或者在其他挂起函数内调用。否则 IDE 就会提示错误。

协程提供了一种避免阻塞线程并用更简单、更可控的操作替代线程阻塞的方法:协程挂起和恢复。
本质上,挂起函数就是一个提醒作用,函数创建者给函数调用者的提醒,表示这是一个比较耗时的任务,被创建者用suspend标记函数,调用者只需把挂起函数放在协程里面,协程会自动调度处理,完成后在原来的位置恢复执行。

参考

https://rengwuxian.com/kotlin-coroutines-1/
https://rengwuxian.com/kotlin-coroutines-2/
https://rengwuxian.com/kotlin-coroutines-3/
Kotlin 协程实战进阶(一、筑基篇)
Kotlin 协程实战进阶(二、进阶篇)
Kotlin 协程实战进阶(三、原理篇)

1、协程的基本使用
2、协程的上下文理解
3、协程的作用域管理
4、协程的常见进阶使用

Kotlin进阶-高阶函数进阶演变之路

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注