Kotlin协程是怎么来简化并发操作的(挂起和恢复)(二)

环境配置

    //依赖协程核心库 ,提供Android UI调度器
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9"
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9"

开始使用

先看第一个例子:

  GlobalScope.launch { // 在后台启动一个新的协程并继续
                delay(1000L) // 非阻塞的等待 1 秒钟(默认时间单位是毫秒)
           Log.e("hyh","thread-->${Thread.currentThread().name}")// 在延迟后打印输出
//输出:thread-->DefaultDispatcher-worker-1
            }

咱们一点一点来解析,先看GlobalScope

GlobalScope

public object GlobalScope : CoroutineScope {
    /**
     * Returns [EmptyCoroutineContext].
     */
    override val coroutineContext: CoroutineContext
        get() = EmptyCoroutineContext
}

他就是一个静态类,继承了CoroutineScope

public interface CoroutineScope {
    /**
     * The context of this scope.
     * Context is encapsulated by the scope and used for implementation of coroutine builders that are extensions on the scope.
     * Accessing this property in general code is not recommended for any purposes except accessing the [Job] instance for advanced usages.
     *
     * By convention, should contain an instance of a [job][Job] to enforce structured concurrency.
     */
    public val coroutineContext: CoroutineContext
}

里面就一个CoroutineContext变量,而是还是空的,这个CoroutineContext现在可以理解为当前协程环境,还是回到GlobalScope 类,先初始化一个EmptyCoroutineContext,大家记住这个coroutineContext变量就行,具体什么用处后面会讲解。

launch

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
}

这是CoroutineScope的扩展函数,大家就会问那为什么CoroutineScope.launch这样调用呢,看CoroutineScope源码,CoroutineScope就是一个接口类,不能new又不是静态类,所以没办法直接调用,那为什么不把扩展函数写到GlobalScope 里面,因为实现CoroutineScope接口类不止这一个类,还有其他开启协程的方式,后续会讲解。
有三个参数分别为:

  1. CoroutineContext:当前协程环境,设置线程环境
  2. CoroutineStart:是一个枚举变量,先不关心
  3. suspend CoroutineScope.() -> Unit:一个回调函数
    这三个参数可以先初步这样理解,后续讲解源码时,会着重介绍,这样就开启了一个协程,从打印里面的线程号可以得到,当前协程是运行在一个子线程里面,开启一个协程就是这样方便,上一章咱们讲过,协程最大的优势是简化了并发操作,不使用回调就能做到线程切换,看下面例子:
 GlobalScope.launch(Dispatchers.Main) { // 在后台启动一个新的协程并继续
                Log.e("hyh", "thread-1->${Thread.currentThread().name}")// 在延迟后打印输出
                val result = withContext(Dispatchers.IO) {
                    task1()
                    Log.e("hyh", "thread-2->${Thread.currentThread().name}")// 在延迟后打印输出
                }
                Log.e("hyh", "thread-3->${Thread.currentThread().name}")// 在延迟后打印输出
            }

  private fun task1(): String {
        Thread.sleep(1000)
        return "111"

    }
打印日志:
: thread-1->main
: thread-2->DefaultDispatcher-worker-1
: thread-3->main

看到这里是不是就有疑惑,“3”怎么会延迟打印呢,这不相当于把主线程阻塞了,主要操作是withContext函数里面的操作,实际上直接讲这个函数里面有很多函数难理解,咱们换个写法:

GlobalScope.launch(Dispatchers.Main) { // 在后台启动一个新的协程并继续
                Log.e("hyh", "thread-1->${Thread.currentThread().name}")// 在延迟后打印输出
                task1()
                Log.e("hyh", "thread-3->${Thread.currentThread().name}")// 在延迟后打印输出
            }

    private suspend fun task1(): String {
        delay(1000)
        Log.e("hyh", "thread-2->${Thread.currentThread().name}")// 在延迟后打印输出
        return "111"

    }

下面就开始讲解协程最主要的设计理念,协程的挂起和恢复。

挂起与恢复

挂起和恢复定义:
  在协程中,当我们的代码执行到某个位置时,可以使用特定的关键字来暂停函数的执行,同时保存函数的执行状态,这个过程叫做 [挂起],挂起操作会将控制器交还给调用方,调用方可以继续执行其他任务。
  当再次调用被挂起的函数时,它会从上一次暂停的位置开始继续执行,这个过程称为 [恢复]。在恢复操作之后,被挂起的函数会继续执行之前保存的状态,从而可以在不重新计算的情况下继续执行之前的逻辑。
  在kotlin中,被suspend 修饰符标记的函数就是挂起函数,它可能会通过调用其他挂起函数挂起执行代码,而不阻塞当前执行线程。挂起函数不能在常规代码中被调用,只能在其他挂起函数。但并不是说加了这个关键字就一定会挂起,suspend 只是作为一个标记,用于告诉编译器,该函数可能会挂起并暂停执行,具体是否挂起就看函数里面的是否有挂起操作。
  从上面的例子可以看到,delay函数也是一个挂起函数,里面有具体的挂起动作。先看一下挂起函数的java实现:

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

         @Nullable
         public final Object invokeSuspend(@NotNull Object $result) {
            Object var2 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
            StringBuilder var10001;
            Thread var10002;
            switch(this.label) {
            case 0:
               ResultKt.throwOnFailure($result);
               var10001 = (new StringBuilder()).append("launch--1--thread-1->");
               var10002 = Thread.currentThread();
               Intrinsics.checkNotNullExpressionValue(var10002, "Thread.currentThread()");
               Log.e("hyh", var10001.append(var10002.getName()).toString());
               MainActivity var10000 = MainActivity.this;
               this.label = 1;
               if (var10000.task1(this) == var2) {
                  return var2;
               }
               break;
            case 1:
               ResultKt.throwOnFailure($result);
               break;
            default:
               throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
            }

            var10001 = (new StringBuilder()).append("launch--1--thread-3->");
            var10002 = Thread.currentThread();
            Intrinsics.checkNotNullExpressionValue(var10002, "Thread.currentThread()");
            Log.e("hyh", var10001.append(var10002.getName()).toString());
            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);
         }
      }), 2, (Object)null);

 private final Object task1(Continuation var1) {
      Object $continuation;
      label20: {
         if (var1 instanceof <undefinedtype>) {
            $continuation = (<undefinedtype>)var1;
            if ((((<undefinedtype>)$continuation).label & Integer.MIN_VALUE) != 0) {
               ((<undefinedtype>)$continuation).label -= Integer.MIN_VALUE;
               break label20;
            }
         }

         $continuation = new ContinuationImpl(var1) {
            // $FF: synthetic field
            Object result;
            int label;

            @Nullable
            public final Object invokeSuspend(@NotNull Object $result) {
               this.result = $result;
               this.label |= Integer.MIN_VALUE;
               return MainActivity.this.task1(this);
            }
         };
      }

      Object $result = ((<undefinedtype>)$continuation).result;
      Object var4 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
      switch(((<undefinedtype>)$continuation).label) {
      case 0:
         ResultKt.throwOnFailure($result);
         ((<undefinedtype>)$continuation).label = 1;
         if (DelayKt.delay(2000L, (Continuation)$continuation) == var4) {
            return var4;
         }
         break;
      case 1:
         ResultKt.throwOnFailure($result);
         break;
      default:
         throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
      }

      StringBuilder var10001 = (new StringBuilder()).append("launch--1--thread-2->");
      Thread var10002 = Thread.currentThread();
      Intrinsics.checkNotNullExpressionValue(var10002, "Thread.currentThread()");
      Log.e("hyh", var10001.append(var10002.getName()).toString());
      return "111";
   }

  从上面代码可以看到suspend修饰的方法会加入一个Continuation 类(续体)信息,关于怎么创建Continuation类的,Kotlin源码可读性不是太好,这个地方后续会进行专门讲解,大家可以先理解成,Continuation 就是一个callback类,执行步骤为:

  1. 先配置一些环境参数,并且创建Continuation(这是个接口,内部有具体的实现类),从外部(内部调用逻辑很复杂,可以先不操心)看调用流程是:invoke-》create-》invokeSuspend,协程体内的代码都在invokeSuspend,相当于协程内执行的代码是在invokeSuspend里面
    2:执行到invokeSuspend里面,里面有一个switch用来判断状态,这个就是状态机,根据不同的状态执行不懂的代码逻辑,刚开始时状态为0,先执行挂起点之前的代码,执行完以后,进入挂起函数task1
    3.task1形参里面会让传入Continuation,执行到delay之前时,还没有进行挂起,所以状态码为0,开始执行delay,由于delay是真正能挂起的函数,会返回一个状态码为:IntrinsicsKt.getCOROUTINE_SUSPENDED(),代表已挂起,然后返回,这个时候会再次进入第一个挂起函数
if (var10000.task1(this) == var2) {
                  return var2;
               }

,协程方法已经结束,当前协程方法已经挂起,大家可以看到当前方法里面还有代码没执行,所以说协程的挂起本质上是方法的挂起,而方法的挂起本质是return。
4.当真正挂起函数执行完后,会通过Continuation调用到invokeSuspend方法,由于当前的状态码为1,所以就会执行剩余的代码,所以说协程的恢复本质上方法的恢复,而恢复的本质是 callback 回调。
  关于是怎么实现的,后续会讲解具体的源码实现,学到这里基本上已经知道协程的挂起和恢复基本理念了。
  我当时学到这里的时候,还发现一个问题,协程体是运行在线程里面的,当挂起时,当前线程在干什么,恢复时是恢复到哪个线程里面?
  先讲解IO线程:

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        GlobalScope.launch(Dispatchers.IO) { // 在后台启动一个新的协程并继续
            Log.e("hyh", "launch--1--thread-1->${Thread.currentThread().name}")// 在延迟后打印输出
            task1()
            Log.e("hyh", "launch--1--thread-3->${Thread.currentThread().name}")// 在延迟后打印输出
        }
        Thread.sleep(1000)
        GlobalScope.launch(Dispatchers.IO) { // 在后台启动一个新的协程并继续
            Log.e("hyh", "launch--2--thread-1->${Thread.currentThread().name}")// 在延迟后打印输出
           Thread.sleep(3000)
        }
    }


    private suspend fun task1(): String {
        delay(2000)
        Log.e("hyh", "launch--1--thread-2->${Thread.currentThread().name}")// 在延迟后打印输出
        return "111"
    }

上面是我写的的测试代码,执行结果为:

第一次运行结果:
 launch--1--thread-1->DefaultDispatcher-worker-1
launch--2--thread-1->DefaultDispatcher-worker-3
 launch--1--thread-2->DefaultDispatcher-worker-1
 launch--1--thread-3->DefaultDispatcher-worker-1
第二次运行结果:
launch--1--thread-1->DefaultDispatcher-worker-1
 launch--2--thread-1->DefaultDispatcher-worker-1
 launch--1--thread-2->DefaultDispatcher-worker-3
launch--1--thread-3->DefaultDispatcher-worker-3

从这两次结果接可以看到,当前协程所在的线程会空置出来,就进入到闲置线程队列里面,如果还有一个线程需要执行协程代码,就可以使用当前闲置的线程,恢复时,也是会检测到哪个线程闲置,就会使用闲置线程来执行,这里要注意,主线程恢复逻辑不是这样的,挂起点是在主线程时,恢复时也会在主线程恢复。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,295评论 6 512
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,928评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,682评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,209评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,237评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,965评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,586评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,487评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,016评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,136评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,271评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,948评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,619评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,139评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,252评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,598评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,267评论 2 358

推荐阅读更多精彩内容