Exploring Kotlin coroutines

What are Kotlin coroutines?

This article is just a summary of the understanding of Kotlin coroutines after my own research. If there is any deviation, please correct me.

Brief summary:

Coroutine is a set of thread API framework provided by Kotlin, which can easily switch threads. And without worrying about thread scheduling, you can easily do concurrent programming. It can also be said that the coroutine is a concurrent design pattern.

The following is the use of traditional threads and coroutines to perform tasks:

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

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

In actual application development, it is usually to start sub-threads in the main thread to perform time-consuming tasks, wait for the execution of time-consuming tasks to complete, then send the results to the main thread, and then refresh the 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)
        }

As can be seen from the above, using Java Threadand Executorsrequires manual thread switching. This kind of code is not only inelegant, but also has an important problem, that is, to deal with the context judgment related to the life cycle, which makes the logic complicated. And error prone.

RxJava is an elegant asynchronous processing framework with simplified code logic, high readability and maintainability, which helps us handle thread switching operations very well. This is even more powerful when developed in the Java language environment, but developed in the Kotlin language environment, today's coroutines are more convenient than RxJava, or have more advantages.

Let's look at an example of using coroutines in 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]

In the code above:

  • launchis a function that creates a coroutine and dispatches the execution of its function body to the appropriate scheduler.
  • Dispatchers.MAINIndicates that this coroutine should execute on the main thread reserved for UI operations.
  • Dispatchers.IOIndicates that this coroutine should execute on a thread reserved for I/O operations.
  • withContext(Dispatchers.IO)Move the execution of the coroutine to an I/O thread.

From the console output, it can be seen that when calculating the natural number sum of 1-50 and 51-100, the thread is Thread[main,5,main]switched from the main thread ( ) to the thread of the coroutine ( DefaultDispatcher-worker-1,5,main), here the calculation of 1-50 and 51-100 They are all the same sub-thread.

There is an important phenomenon here. The code looks synchronous logically, and when the coroutine is started to perform tasks, the main thread is not blocked to continue to perform related operations, and after the asynchronous task execution in the coroutine is completed, it automatically Switched back to the main thread. This is the benefit that Kotlin coroutines bring to development and concurrent programming. This is also the source of a concept: Kotlin coroutines are synchronous and non-blocking .

Is "synchronous non-blocking " really " synchronous non-blocking "? Let's explore the tricks and see the above code in the .class file through Android Studio:

      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());

Although the code in the .class file above is more complicated, it can be seen from the general logic that the Kotlin coroutine also implements asynchronous operations through the callback interface, which also explains that the Kotlin coroutine only makes the code logic synchronous and non-blocking, but the actual There is no above, but the Kotlin compiler does a lot of things for the code, which is why Kotlin coroutines are actually a set of thread API frameworks.

Here's another variation on the above example:

        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]

asyncA coroutine is created that makes calculating the sum of natural numbers 1-50 and 51-100 two concurrent operations . From the console output above, it can be seen that the calculation of the natural number sum of 1-50 is Thread[DefaultDispatcher-worker-2,5,main]in , while the calculation of the natural number sum of 51-100 is in another thread Thread[DefaultDispatcher-worker-1,5,main].

From the above example, the coroutine is in asynchronous operation, that is, thread switching: the main thread starts the sub-thread to perform time-consuming operations, and when the time-consuming operation is completed and the result is updated to the main thread, the code logic is simplified and the readability is high. .

What is suspend?

The literal translation of suspend is: Suspend
suspend is a keyword in the Kotlin language used to modify a method. When a method is modified, it means that this method can only be called by the method modified by suspend or in a coroutine.
Let's take a look at splitting the above code case into several suspend methods:

    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]

When you mark a method with the suspend keyword, you actually tell Kotlin to call the method from within a coroutine. So this "suspension" does not mean that the method or function is suspended, nor does it mean that the thread is suspended.

What if a non-suspend-modified method calls a suspend-modified method?

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

At this point, the compiler will prompt:

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

Let's look at the above method calcNumbers50To100Sum code in the .class file:

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

It can be seen private suspend fun calcNumbers50To100Sum()that after being compiled by the Kotlin compiler, it becomes private final Object calcNumbers50To100Sum(Continuation $completion), suspenddisappears, and the method has one more parameter Continuation $completion, so the compiler will do special processing for the method or function that is suspendmodified in Kotlin.

In addition, suspendthe modified method also indicates that this method is a time-consuming method, telling the method caller to use a coroutine. When suspendthe method , it also indicates that the thread needs to be switched. At this time, the main thread can still continue to execute, but the code in the coroutine may be suspended.

Here's a slightly modified calcNumbers50To100Summethod :

   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]

The main thread is not affected by coroutine threads.

Summarize

Kotlin coroutine is a set of thread API framework. Using it in Kotlin language environment for concurrent programming has more advantages than traditional Thread, Executors and RxJava. The code logic is "synchronous and non-blocking", and it is concise, easy to read and maintain.

suspendis a keyword in the Kotlin language, used to decorate a method, when the method is decorated, the method can only be called by suspendthe decorated method and coroutine. At this time, it also indicates that this method is a time-consuming method, telling the caller that it needs to be used in a coroutine.

Reference documents:

  1. Kotlin coroutines on Android
  2. Coroutines guide

In the next article, we will study Kotlin Flow.

Guess you like

Origin blog.csdn.net/wangjiang_qianmo/article/details/128520879