Kotlin 入门到进阶(补充内容4) -- 协程 Coroutine 知识点

协程使用说明:https://blog.csdn.net/tmacfrank/article/details/145084326

协程原理分析:
协程是用户态的线程运行框架,并不是直接操作操作系统级别的线程对象,大部分情况下可以不涉及线程的销毁、创建、回收、线程调度等操作系统级别的操作,所以协程比线程对操作系统的资源消耗更少,运行效率也更高

    CoroutineScope(Dispatchers.Default).launch {
        println("${Thread.currentThread().name}: test")
    }
    Thread.sleep(100)

输出打印:
    DefaultDispatcher-worker-1: test

上面为最简单的协程使用例子,作用域(CoroutineScope)指定一种类型的派发器(Dispatchers),lambda表达式里面的任务就会在所指定的线程/线程池里面执行,launch返回一个 Job 对象,Job 对象可以获取协程任务的运行状态、开始或者取消协程

一:协程上下文(CoroutineContext)

协程上下文的作用:资源获取,配置管理等工作,是执行环境的通用数据资源的统一管理者。是协程运行必须设置的参数。
以下对象本质上都是协程上下文

  • Job 协程句柄
  • CoroutineName 协程名称
  • CoroutineDispatcher 协程调度器
  • CoroutineExceptionHandler 协程异常处理器

launch函数里面实现了操作符重载,支持以上多种类型的上下文进行 + 操作,结合后的协程上下文变成 [xxxxxxx,xxxxxxxx,xxxxxxxx] 形式

public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job {
    val newContext = newCoroutineContext(context)
    val coroutine = if (start.isLazy)
        LazyStandaloneCoroutine(newContext, block) else
        StandaloneCoroutine(newContext, active = true)
    coroutine.start(start, coroutine, block)
    return coroutine
}

第一个参数是协程上下文,可以指定 Dispatcher 类型

派发器的分类:
* Dispatchers.Default : 默认的派发器,会将协程派发到默认的线程池进行调度和执行
* Dispatchers.Main : 主线程派发器,会将协程派发到主线程调度和执行,Kotlin 默认没有实现该调度器,需要实际的接入方提供具体的实现,针对 Android 开发来说,需要额外添加 kotlinx-coroutines-android 的运行时依赖
* Dispatchers.Unconfined : 未限定协程执行线程的派发器,该派发器会在调用线程中执行该协程,当协程被挂起后,恢复时所在的线程由挂起函数决定(在挂起时,由挂起函数决定要将该协程派发到哪个线程执行)
* Dispatchers.IO : IO 派发器,和 Default 派发器共用执行线程池,默认为 64 和虚拟机中可用的处理器核心数中的最大值

第二个参数是调度策略,可以指定 CoroutineStart 类型

调度策略分类:
* CoroutineStart.DEFAULT : 默认的调度策略,会立刻将协程调度进入目标线程/线程池中的任务队列,等待执行,在实际执行之前可以通过 Job.cancel 方法取消。
* CoroutineStart.LAZY: 懒调度策略,只有当协程需要被执行的时候才会进行调度,调用方可以通过 Job.start 方法来触发调度
* CoroutineStart. ATOMIC: 自动调度,和 DEFAULT 策略类似,会立刻调度协程到任务队列中,区别在于该调度策略在协程实际执行前无法取消
* CoroutineStart.UNDISPATCHED: 立刻在当前调用线程中执行该协程,直到协程遇到第一个挂起函数,恢复时根据协程上下文中的派发器来决定要将协程派发到哪个线程/线程池中继续执行。类似于 Unconfined 派发器的作用,区别在于该策略在协程挂起并恢复后的执行线程由协程派发器决定。

二:调度原理

关键路径

CoroutineStart.invoke -> CancellableKt.startCoroutineCancellable -> 
生成 Continuation 对象 -> resumeCancellableWith -> 进入 dispatch 任务分发流程 -> 
CoroutineScheduler. dispatch -> 创建任务、将任务添加到队列、执行任务 -> DispatchedTask.run -> 
BaseContinuationImpl.resumeWith -> BaseContinuationImpl. invokeSuspend -> 执行 挂起函数,就是外部传进来的 lambda 表达式

反编译后,可以看到最终执行的是 invokSuspend 函数

public final class CoroutinePrincipleKt {
   public static final void main() {
      Job job = BuildersKt.launch(CoroutineScopeKt.CoroutineScope((CoroutineContext)EmptyCoroutineContext.INSTANCE), (CoroutineContext)Dispatchers.getIO(), CoroutineStart.LAZY, (Function2)(new Function2((Continuation)null) {
         int label;

         @Nullable
         public final Object invokeSuspend(@NotNull Object var1) {
            Object var3 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
            switch (this.label) {
               case 0:
                  ResultKt.throwOnFailure(var1);
                  StringBuilder var10000 = new StringBuilder();
                  Thread var10001 = Thread.currentThread();
                  Intrinsics.checkNotNullExpressionValue(var10001, "Thread.currentThread()");
                  String var2 = var10000.append(var10001.getName()).append(": test11").toString();
                  System.out.println(var2);
                  return Unit.INSTANCE;
               default:
                  throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
            }
         }

         @NotNull
         public final Continuation create(@Nullable Object value, @NotNull Continuation completion) {
            Intrinsics.checkNotNullParameter(completion, "completion");
            Function2 var3 = new <anonymous constructor>(completion);
            return var3;
         }

         public final Object invoke(Object var1, Object var2) {
            return ((<undefinedtype>)this.create(var1, (Continuation)var2)).invokeSuspend(Unit.INSTANCE);
         }
      }));
      job.start();
      Thread.sleep(100L);
   }

   // $FF: synthetic method
   public static void main(String[] var0) {
      main();
   }
}

三:挂起、恢复

协程之所以高效的原因

    CoroutineScope(Dispatchers.Default).launch {
        println("${Thread.currentThread().id} : coroutine1 log 1")
        delay(200)
        print("${Thread.currentThread().id} : coroutine1 log 2")
    }

    Thread.sleep(80)

    CoroutineScope(Dispatchers.Default).launch {
        println("${Thread.currentThread().id} : coroutine2 log 1")
    }

    Thread.sleep(500)

运行上面的代码后,会出现以下打印

10 : coroutine1 log 1
10 : coroutine2 log 1
10 : coroutine1 log 2

从打印的结果看到,协程做到了在不阻塞当前线程的情况下,实现了任务的挂起,另一个任务可以运行在相同的线程下,并且挂起结束后,挂起任务可以继续在当前线程运行。
用同步的方式,实现了异步的效果,中间不涉及线程切换等操作系统级别的操作。

问题一:为什么协程会知道需要执行另一个任务?
DefaultExecutor 内部有单线程轮询堆顶任务,通过 delay 时间比较,确保能及时运行任务队列里面的任务。如果当前任务等待时间还没到,就会运行其他等待时间更少或者无需等待的任务

问题二:协程怎么知道恢复后,到底需要从哪里继续执行剩余逻辑?

BuildersKt.launch$default(CoroutineScopeKt.CoroutineScope((CoroutineContext)Dispatchers.getDefault()), (CoroutineContext)null, (CoroutineStart)null, (Function2)(new Function2((Continuation)null) {
         int label;

         @Nullable
         public final Object invokeSuspend(@NotNull Object $result) {
            Object var3 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
            StringBuilder var10000;
            Thread var10001;
            String var2;
            switch (this.label) {
               case 0:
                  ResultKt.throwOnFailure($result);
                  var10000 = new StringBuilder();
                  var10001 = Thread.currentThread();
                  Intrinsics.checkNotNullExpressionValue(var10001, "Thread.currentThread()");
                  var2 = var10000.append(var10001.getId()).append(" : coroutine1 log 1").toString();
                  System.out.println(var2);
                  // 执行完第一行 log 的打印,准备执行 delay 函数挂起协程,这里把 label 设置为 1
                  this.label = 1;
                  if (DelayKt.delay(200L, this) == var3) {
                     return var3;
                  }
                  break;
               case 1:
                  ResultKt.throwOnFailure($result);
                  break;
               default:
                  throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
            }

            var10000 = new StringBuilder();
            var10001 = Thread.currentThread();
            Intrinsics.checkNotNullExpressionValue(var10001, "Thread.currentThread()");
            var2 = var10000.append(var10001.getId()).append(" : coroutine1 log 2").toString();
            System.out.print(var2);
            return Unit.INSTANCE;
         }

         @NotNull
         public final Continuation create(@Nullable Object value, @NotNull Continuation completion) {
            Intrinsics.checkNotNullParameter(completion, "completion");
            Function2 var3 = new <anonymous constructor>(completion);
            return var3;
         }

         public final Object invoke(Object var1, Object var2) {
            return ((<undefinedtype>)this.create(var1, (Continuation)var2)).invokeSuspend(Unit.INSTANCE);
         }
      }), 3, (Object)null);

执行完挂起点后,会将 label 自增,实现恢复后执行挂起点之后的逻辑
本质上就是维护一个状态机,通过 label 字段实现多个挂起点的逻辑还原

四、异常处理

        // 创建一个协程异常处理上下文
        val coroutineExceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
            val coroutineName = coroutineContext[CoroutineName]?.name
            println("$coroutineName occurred exception: $throwable")
        }
        CoroutineScope(Dispatchers.Default).launch(coroutineExceptionHandler + CoroutineName("My coroutine")) {
            println("${Thread.currentThread().name}: coroutine test 1")
            throw RuntimeException("test exception")
        }

        Thread.sleep(1000)

打印结果:

DefaultDispatcher-worker-1: coroutine test 1
My coroutine occurred exception: java.lang.RuntimeException: test exception

如果外部不设置异常处理逻辑,协程内部会有默认的异常兜底

五、协程作用域

CoroutineScope 的作用是方便批量管理协程,每一个作用域都有一个根 Job,作用域内创建的协程会作为子 Job 添加到根 Job 上。
如果根 Job 被取消,则这个作用域下的 Job 对象,也会被取消。

作用域的分类:

* runBlocking:顶层函数,它的第二个参数为接收者是CoroutineScope的函数字面量,可启动协程。但是它会阻塞当前线程,主要用于测试。
* GlobalScope:全局协程作用域,通过GlobalScope创建的协程不会有父协程,可以把它称为根协程。它启动的协程的生命周期只受整个应用程序的生命周期的限制,且不能取消,在运行时会消耗一些内存资源,这可能会导致内存泄露,所以仍不适用于业务开发。
* coroutineScope:创建一个独立的协程作用域,直到所有启动的协程都完成后才结束自身。它是一个挂起函数,需要运行在协程内或挂起函数内。当这个作用域中的任何一个子协程失败时,这个作用域失败,所有其他的子程序都被取消。为并行分解工作而设计的。
* supervisorScope:与coroutineScope类似,不同的是子协程的异常不会影响父协程,也不会影响其他子协程。(作用域本身的失败(在block或取消中抛出异常)会导致作用域及其所有子协程失败,但不会取消父协程。)
* MainScope:为UI组件创建主作用域。一个顶层函数,上下文是SupervisorJob() + Dispatchers.Main,说明它是一个在主线程执行的协程作用域,通过cancel对协程进行取消。推荐使用。

如果是Android运行环境,还有

* lifecycleScope:Lifecycle Ktx库提供的具有生命周期感知的协程作用域,与Lifecycle绑定生命周期,生命周期被销毁时,此作用域将被取消。会与当前的UI组件绑定生命周期,界面销毁时该协程作用域将被取消,不会造成协程泄漏,推荐使用。
* viewModelScope:与lifecycleScope类似,与ViewModel绑定生命周期,当ViewModel被清除时,这个作用域将被取消。推荐使用。

以上两个 Android 环境的作用域,会和界面、ViewModel 绑定,如果界面销毁、ViewModel 销毁,自身的作用域也会销毁

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容