Kotlin コルーチンの探索

Kotlin コルーチンとは何ですか?

この記事は Kotlin コルーチンについての理解を私自身が調べてまとめたものですので、逸脱がある場合はご指摘ください。

簡単な概要:

Coroutine は、Kotlin が提供するスレッド API フレームワークのセットで、スレッドを簡単に切り替えることができます。また、スレッドのスケジュールを気にすることなく、並行プログラミングを簡単に行うことができます。コルーチンは並行設計パターンであるとも言えます。

以下は、従来のスレッドとコルーチンを使用してタスクを実行する方法です。

       Thread{
            //执行耗时任务
        }.start()

        val executors = Executors.newCachedThreadPool()
        executors.execute {
          //执行耗时任务
        }
        
       GlobalScope.launch(Dispatchers.IO) {
          //执行耗时任务
        }

実際のアプリケーション開発では、通常、メインスレッドでサブスレッドを開始して時間のかかるタスクを実行し、時間のかかるタスクの実行が完了するのを待ち、結果をメインスレッドに送信してから UI を更新します。 :

       Thread{
            //执行耗时任务
            runOnMainThread { 
                //获取耗时任务结果,刷新UI
            }
        }.start()

        val executors = Executors.newCachedThreadPool()
        executors.execute {
            //执行耗时任务
            runOnMainThread {
                //获取耗时任务结果,刷新UI
            }
        }

        Observable.unsafeCreate<Unit> {
            //执行耗时任务
        }.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribe {
            //获取耗时任务结果,刷新UI
        }

        GlobalScope.launch(Dispatchers.Main) {
            val result = withContext(Dispatchers.IO){
                //执行耗时任务
            }
            //直接拿到耗时任务结果,刷新UI
            refreshUI(result)
        }

上記からもわかるように、Java を使用しておりThreadExecutors手動でスレッドを切り替える必要があるため、この種のコードは洗練されていないだけでなく、ライフサイクルに関連するコンテキスト判断を処理するという重要な問題を抱えています。ロジックが複雑で、エラーが発生しやすくなります。

RxJava は、簡素化されたコード ロジック、高い可読性、保守性を備えたエレガントな非同期処理フレームワークであり、スレッド切り替え操作を適切に処理するのに役立ちます。これは Java 言語環境で開発するとさらに強力ですが、Kotlin 言語環境で開発すると、現在のコルーチンは RxJava よりも便利であるか、より多くの利点があります。

Kotlin でのコルーチンの使用例を見てみましょう。

        GlobalScope.launch(Dispatchers.Main) {
            Log.d("TestCoroutine", "launch start: ${Thread.currentThread()}")
            val numbersTo50Sum = withContext(Dispatchers.IO) {
                //在子线程中执行 1-50 的自然数和
                Log.d("TestCoroutine", "launch:numbersTo50Sum: ${Thread.currentThread()}")
                delay(1000)
                val naturalNumbers = generateSequence(0) { it + 1 }
                val numbersTo50 = naturalNumbers.takeWhile { it <= 50 }
                numbersTo50.sum()
            }

            val numbers50To100Sum = withContext(Dispatchers.IO) {
               //在子线程中执行 51-100 的自然数和
                Log.d("TestCoroutine", "launch:numbers50To100Sum: ${Thread.currentThread()}")
                delay(1000)
                val naturalNumbers = generateSequence(51) { it + 1 }
                val numbers50To100 = naturalNumbers.takeWhile { it in 51..100 }
                numbers50To100.sum()
            }

            val result = numbersTo50Sum + numbers50To100Sum
            Log.d("TestCoroutine", "launch end:result=$result ${Thread.currentThread()}")
        }
        Log.d("TestCoroutine", "Hello World!,${Thread.currentThread()}")
控制台输出结果:
2023-01-02 16:05:45.846 10153-10153/com.wangjiang.example D/TestCoroutine: Hello World!,Thread[main,5,main]
2023-01-02 16:05:48.058 10153-10153/com.wangjiang.example D/TestCoroutine: launch start: Thread[main,5,main]
2023-01-02 16:05:48.059 10153-10322/com.wangjiang.example D/TestCoroutine: launch:numbersTo50Sum: Thread[DefaultDispatcher-worker-1,5,main]
2023-01-02 16:05:49.114 10153-10322/com.wangjiang.example D/TestCoroutine: launch:numbers50To100Sum: Thread[DefaultDispatcher-worker-1,5,main]
2023-01-02 16:05:50.376 10153-10153/com.wangjiang.example D/TestCoroutine: launch end:result=5050 Thread[main,5,main]

上記のコードでは次のようになります。

  • launchコルーチンを作成し、その関数本体の実行を適切なスケジューラにディスパッチする関数です。
  • Dispatchers.MAINこのコルーチンが UI 操作用に予約されたメインスレッドで実行される必要があることを示します。
  • Dispatchers.IOこのコルーチンが I/O 操作用に予約されたスレッドで実行される必要があることを示します。
  • withContext(Dispatchers.IO)コルーチンの実行を I/O スレッドに移動します。

Thread[main,5,main]コンソール出力から、1-50と51-100の自然数の和を計算する際にスレッドがメインスレッド( )からコルーチンのスレッド( )に切り替わっていることがわかりますDefaultDispatcher-worker-1,5,main。ここでは1-50の計算を行っています。 50 と 51-100 はすべて同じサブスレッドです。

ここで重要な現象が発生します。コードは論理的には同期しているように見え、コルーチンがタスクの実行を開始しても、メインスレッドは関連する操作の実行を継続するためにブロックされず、コルーチン内の非同期タスクの実行が完了すると、自動的に実行されます。メインスレッドに戻りました。これは、Kotlin コルーチンが開発と同時プログラミングにもたらす利点です。これは概念の源でもあります。Kotlin コルーチンは同期的であり、ノンブロッキングです

「同期ノンブロッキング」は本当に「同期ノンブロッキング」なのでしょうか? トリックを探って、Android Studio を介して .class ファイル内の上記のコードを見てみましょう。

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

         @Nullable
         public final Object invokeSuspend(@NotNull Object $result) {
            Object var10000;
            int numbersTo50Sum;
            label17: {
               Object var5 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
               Function2 var10001;
               CoroutineContext var6;
               switch(this.label) {
               case 0:
                  ResultKt.throwOnFailure($result);
                  Log.d("TestCoroutine", "launch start: " + Thread.currentThread());
                  var6 = (CoroutineContext)Dispatchers.getIO();
                  var10001 = (Function2)(new Function2((Continuation)null) {
                     int label;

                     @Nullable
                     public final Object invokeSuspend(@NotNull Object $result) {
                        Object var4 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
                        switch(this.label) {
                        case 0:
                           ResultKt.throwOnFailure($result);
                           Log.d("TestCoroutine", "launch:numbersTo50Sum: " + Thread.currentThread());
                           this.label = 1;
                           if (DelayKt.delay(1000L, this) == var4) {
                              return var4;
                           }
                           break;
                        case 1:
                           ResultKt.throwOnFailure($result);
                           break;
                        default:
                           throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
                        }

                        Sequence naturalNumbers = SequencesKt.generateSequence(Boxing.boxInt(0), (Function1)null.INSTANCE);
                        Sequence numbersTo50 = SequencesKt.takeWhile(naturalNumbers, (Function1)null.INSTANCE);
                        return Boxing.boxInt(SequencesKt.sumOfInt(numbersTo50));
                     }

                     @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);
                     }
                  });
                  this.label = 1;
                  var10000 = BuildersKt.withContext(var6, var10001, this);
                  if (var10000 == var5) {
                     return var5;
                  }
                  break;
               case 1:
                  ResultKt.throwOnFailure($result);
                  var10000 = $result;
                  break;
               case 2:
                  numbersTo50Sum = this.I$0;
                  ResultKt.throwOnFailure($result);
                  var10000 = $result;
                  break label17;
               default:
                  throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
               }

               numbersTo50Sum = ((Number)var10000).intValue();
               var6 = (CoroutineContext)Dispatchers.getIO();
               var10001 = (Function2)(new Function2((Continuation)null) {
                  int label;

                  @Nullable
                  public final Object invokeSuspend(@NotNull Object $result) {
                     Object var4 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
                     switch(this.label) {
                     case 0:
                        ResultKt.throwOnFailure($result);
                        Log.d("TestCoroutine", "launch:numbers50To100Sum: " + Thread.currentThread());
                        this.label = 1;
                        if (DelayKt.delay(1000L, this) == var4) {
                           return var4;
                        }
                        break;
                     case 1:
                        ResultKt.throwOnFailure($result);
                        break;
                     default:
                        throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
                     }

                     Sequence naturalNumbers = SequencesKt.generateSequence(Boxing.boxInt(51), (Function1)null.INSTANCE);
                     Sequence numbers50To100 = SequencesKt.takeWhile(naturalNumbers, (Function1)null.INSTANCE);
                     return Boxing.boxInt(SequencesKt.sumOfInt(numbers50To100));
                  }

                  @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);
                  }
               });
               this.I$0 = numbersTo50Sum;
               this.label = 2;
               var10000 = BuildersKt.withContext(var6, var10001, this);
               if (var10000 == var5) {
                  return var5;
               }
            }

            int numbers50To100Sum = ((Number)var10000).intValue();
            int result = numbersTo50Sum + numbers50To100Sum;
            Log.d("TestCoroutine", "launch end:result=" + result + ' ' + Thread.currentThread());
            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);
      Log.d("TestCoroutine", "Hello World!," + Thread.currentThread());

上記の .class ファイル内のコードはより複雑ですが、一般的なロジックから、Kotlin コルーチンがコールバック インターフェイスを介して非同期操作も実装していることがわかります。これは、Kotlin コルーチンがコード ロジックを同期化および非同期化するだけであることも説明します。上記はありませんが、Kotlin コンパイラーはコードに対して多くのことを行います。そのため、Kotlin コルーチンは実際にはスレッド API フレームワークのセットです。

上記の例の別のバリエーションを次に示します。

        GlobalScope.launch(Dispatchers.Main) {
            Log.d("TestCoroutine", "launch start: ${Thread.currentThread()}")
            val numbersTo50Sum = async {
                withContext(Dispatchers.IO) {
                    Log.d("TestCoroutine", "launch:numbersTo50Sum: ${Thread.currentThread()}")
                    delay(2000)
                    val naturalNumbers = generateSequence(0) { it + 1 }
                    val numbersTo50 = naturalNumbers.takeWhile { it <= 50 }
                    numbersTo50.sum()
                }
            }

            val numbers50To100Sum = async {
                withContext(Dispatchers.IO) {
                    Log.d("TestCoroutine", "launch:numbers50To100Sum: ${Thread.currentThread()}")
                    delay(500)
                    val naturalNumbers = generateSequence(51) { it + 1 }
                    val numbers50To100 = naturalNumbers.takeWhile { it in 51..100 }
                    numbers50To100.sum()
                }
            }
            // 计算 1-50 和 51-100 的自然数和是两个并发操作
            val result = numbersTo50Sum.await() + numbers50To100Sum.await()
            Log.d("TestCoroutine", "launch end:result=$result ${Thread.currentThread()}")
        }
        Log.d("TestCoroutine", "Hello World!,${Thread.currentThread()}")
   
  控制台输出结果:
2023-01-02 16:32:12.637 13303-13303/com.wangjiang.example D/TestCoroutine: Hello World!,Thread[main,5,main]
2023-01-02 16:32:13.120 13303-13303/com.wangjiang.example D/TestCoroutine: launch start: Thread[main,5,main]
2023-01-02 16:32:14.852 13303-13444/com.wangjiang.example D/TestCoroutine: launch:numbersTo50Sum: Thread[DefaultDispatcher-worker-2,5,main]
2023-01-02 16:32:14.853 13303-13443/com.wangjiang.example D/TestCoroutine: launch:numbers50To100Sum: Thread[DefaultDispatcher-worker-1,5,main]
2023-01-02 16:32:17.462 13303-13303/com.wangjiang.example D/TestCoroutine: launch end:result=5050 Thread[main,5,main]

async1 ~ 50 と 51 ~ 100 の自然数の合計を計算する 2 つの同時操作を実行するコルーチンが作成されます上記のコンソール出力から、1 ~ 50 の自然数の合計の計算が 1 つのスレッドThread[DefaultDispatcher-worker-2,5,main]Thread[DefaultDispatcher-worker-1,5,main]

上記の例から、コルーチンは非同期操作、つまりスレッド切り替え中です。メインスレッドは時間のかかる操作を実行するためにサブスレッドを開始し、時間のかかる操作が完了して結果がメインスレッドに更新されると、スレッド化することでコードロジックが簡素化され、可読性が高くなります。

一時停止とは何ですか?

サスペンドの文字通りの翻訳は次のとおりです: サスペンド
サスペンドはメソッドを変更するために使用される Kotlin 言語のキーワードです。メソッドが変更されると、このメソッドはサスペンドまたはコルーチンで変更されたメソッドによってのみ呼び出すことができることを意味します。
上記のコードケースをいくつかのサスペンドメソッドに分割して見てみましょう。

    fun getNumbersTo100Sum() {
        GlobalScope.launch(Dispatchers.Main) {
            Log.d("TestCoroutine", "launch start: ${Thread.currentThread()}")
            val result = calcNumbers1To100Sum()
            Log.d("TestCoroutine", "launch end:result=$result ${Thread.currentThread()}")
        }
        Log.d("TestCoroutine", "Hello World!,${Thread.currentThread()}")
    }

    private suspend fun calcNumbers1To100Sum(): Int {
        return calcNumbersTo50Sum() + calcNumbers50To100Sum()
    }

    private suspend fun calcNumbersTo50Sum(): Int {
        return withContext(Dispatchers.IO) {
            Log.d("TestCoroutine", "launch:numbersTo50Sum: ${Thread.currentThread()}")
            delay(1000)
            val naturalNumbers = generateSequence(0) { it + 1 }
            val numbersTo50 = naturalNumbers.takeWhile { it <= 50 }
            numbersTo50.sum()
        }
    }

    private suspend fun calcNumbers50To100Sum(): Int {
        return withContext(Dispatchers.IO) {
            Log.d("TestCoroutine", "launch:numbers50To100Sum: ${Thread.currentThread()}")
            delay(1000)
            val naturalNumbers = generateSequence(51) { it + 1 }
            val numbers50To100 = naturalNumbers.takeWhile { it in 51..100 }
            numbers50To100.sum()
        }
    }
控制台输出结果:
2023-01-03 14:47:57.047 11349-11349/com.wangjiang.example D/TestCoroutine: Hello World!,Thread[main,5,main]
2023-01-03 14:47:59.311 11349-11349/com.wangjiang.example D/TestCoroutine: launch start: Thread[main,5,main]
2023-01-03 14:47:59.312 11349-11537/com.wangjiang.example D/TestCoroutine: launch:numbersTo50Sum: Thread[DefaultDispatcher-worker-3,5,main]
2023-01-03 14:48:00.336 11349-11535/com.wangjiang.example D/TestCoroutine: launch:numbers50To100Sum: Thread[DefaultDispatcher-worker-1,5,main]
2023-01-03 14:48:01.339 11349-11349/com.wangjiang.example D/TestCoroutine: launch end:result=5050 Thread[main,5,main]

メソッドに stop キーワードをマークすると、実際にはコルーチン内からメソッドを呼び出すように Kotlin に指示することになります。したがって、この「一時停止」は、メソッドや関数が一時停止されることを意味するものではなく、スレッドが一時停止されることを意味するものでもありません。

サスペンド変更されていないメソッドがサスペンド変更されたメソッドを呼び出した場合はどうなるでしょうか?

  private fun calcNumbersTo100Sum(): Int {
        return calcNumbersTo50Sum() + calcNumbers50To100Sum()
    }

この時点で、コンパイラは次のプロンプトを表示します。

Suspend function 'calcNumbersTo50Sum' should be called only from a coroutine or another suspend function
Suspend function 'calcNumbers50To100' should be called only from a coroutine or another suspend function

.class ファイル内の上記のメソッド calcNumbers50To100Sum コードを見てみましょう。

   private final Object calcNumbers50To100Sum(Continuation $completion) {
      return BuildersKt.withContext((CoroutineContext)Dispatchers.getIO(), (Function2)(new Function2((Continuation)null) {
         int label;

         @Nullable
         public final Object invokeSuspend(@NotNull Object $result) {
            Object var4 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
            switch(this.label) {
            case 0:
               ResultKt.throwOnFailure($result);
               Log.d("TestCoroutine", "launch:numbers50To100Sum: " + Thread.currentThread());
               this.label = 1;
               if (DelayKt.delay(1000L, this) == var4) {
                  return var4;
               }
               break;
            case 1:
               ResultKt.throwOnFailure($result);
               break;
            default:
               throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
            }

            Sequence naturalNumbers = SequencesKt.generateSequence(Boxing.boxInt(51), (Function1)null.INSTANCE);
            Sequence numbers50To100 = SequencesKt.takeWhile(naturalNumbers, (Function1)null.INSTANCE);
            return Boxing.boxInt(SequencesKt.sumOfInt(numbers50To100));
         }

         @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);
         }
      }), $completion);
   }

Kotlin コンパイラーによってコンパイルされた後、 になり、消え、メソッドにはパラメーターが 1 つ増えていることが private suspend fun calcNumbers50To100Sum()わかります。そのため、コンパイラーは、Kotlin で変更された。private final Object calcNumbers50To100Sum(Continuation $completion)suspendContinuation $completionsuspend

さらに、suspend変更されたメソッドは、このメソッドが時間のかかるメソッドであることも示し、メソッド呼び出し元にコルーチンを使用するように指示します。suspendメソッドが実行されると、スレッドを切り替える必要があることも示されます。この時点では、メインスレッドは引き続き実行できますが、コルーチン内のコードは一時停止される可能性があります。

少し変更したcalcNumbers50To100Sum方法。

   private suspend fun calcNumbers50To100Sum(): Int {
        Log.d("TestCoroutine", "launch:numbers50To100Sum:start: ${Thread.currentThread()}")
        val sum= withContext(Dispatchers.Main) {
            Log.d("TestCoroutine", "launch:numbers50To100Sum: ${Thread.currentThread()}")
            delay(1000)
            val naturalNumbers = generateSequence(51) { it + 1 }
            val numbers50To100 = naturalNumbers.takeWhile { it in 51..100 }
            numbers50To100.sum()
        }
        Log.d("TestCoroutine", "launch:numbers50To100Sum:end: ${Thread.currentThread()}")
        return sum
    }
控制台输出结果:
2023-01-03 15:28:04.349 15131-15131/com.bilibili.studio D/TestCoroutine: Hello World!,Thread[main,5,main]
2023-01-03 15:28:04.803 15131-15131/com.bilibili.studio D/TestCoroutine: launch start: Thread[main,5,main]
2023-01-03 15:28:04.804 15131-15266/com.bilibili.studio D/TestCoroutine: launch:numbersTo50Sum: Thread[DefaultDispatcher-worker-3,5,main]
2023-01-03 15:28:06.695 15131-15131/com.bilibili.studio D/TestCoroutine: launch:numbers50To100Sum:start: Thread[main,5,main]
2023-01-03 15:28:06.696 15131-15131/com.bilibili.studio D/TestCoroutine: launch:numbers50To100Sum: Thread[main,5,main]
2023-01-03 15:28:07.700 15131-15131/com.bilibili.studio D/TestCoroutine: launch:numbers50To100Sum:end: Thread[main,5,main]
2023-01-03 15:28:07.700 15131-15131/com.bilibili.studio D/TestCoroutine: launch end:result=5050 Thread[main,5,main]

メインスレッドはコルーチンスレッドの影響を受けません。

要約する

Kotlin コルーチンはスレッド API フレームワークのセットです。Kotlin 言語環境で同時プログラミングに使用すると、従来の Thread、Executor、RxJava よりも多くの利点があります。コード ロジックは「同期かつノンブロッキング」で、簡潔で読みやすいです。そして維持します。

suspendKotlin 言語のキーワードであり、メソッドを装飾するために使用されます。メソッドが装飾されている場合、メソッドはsuspend装飾された。この時点で、このメソッドが時間のかかるメソッドであることも示し、コルーチンで使用する必要があることを呼び出し元に伝えます。

参照文書:

  1. Android 上の Kotlin コルーチン
  2. コルーチンガイド

次の記事では、Kotlin Flow について学習します。

おすすめ

転載: blog.csdn.net/wangjiang_qianmo/article/details/128520879