SoFunction
Updated on 2025-03-01

Kotlin's exploration of what coroutines are

What is Kotlin coroutine

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

Brief summary:

Coroutines are a set of thread API frameworks provided by Kotlin, which can be used to switch threads very easily. And without caring about thread scheduling, you can easily do concurrent programming. It can also be said that coroutines are a concurrent design pattern.

Here are the tasks performed using traditional threads and coroutines:

       Thread{
            //Execute time-consuming tasks        }.start()
        val executors = ()
         {
          //Execute time-consuming tasks        }
       () {
          //Execute time-consuming tasks        }

In actual application development, usually, the child thread is started in the main line to execute time-consuming tasks. When the time-consuming task is completed, the result is given to the main thread and then the UI is refreshed:

       Thread{
            //Execute time-consuming tasks            runOnMainThread { 
                //Get time-consuming task results and refresh the UI            }
        }.start()
        val executors = ()
         {
            //Execute time-consuming tasks            runOnMainThread {
                //Get time-consuming task results and refresh the UI            }
        }
        <Unit> {
            //Execute time-consuming tasks        }.subscribeOn(()).observeOn(()).subscribe {
            //Get time-consuming task results and refresh the UI        }
        () {
            val result = withContext(){
                //Execute time-consuming tasks            }
            // Get the time-consuming task results directly and refresh the UI            refreshUI(result)
        }

As can be seen from above, the JavaThreadandExecutorsAll of them need to manually handle thread switching. Such code is not only inelegant, but also has an important problem, that is, it needs to deal with context judgments related to life cycles, which leads to the logic becoming complex and prone to errors.

RxJava is an elegant asynchronous processing framework, with simplified code logic, high readability and maintainability, and it helps us handle thread switching operations very well. This is a great addition to the development of the Java locale, but when developed in the Kotlin locale, coroutines are now more convenient or more advantageous than RxJava.

Here is an example of using coroutines in Kotlin:

        () {
            ("TestCoroutine", "launch start: ${()}")
            val numbersTo50Sum = withContext() {
                //Execute the natural sum of 1-50 in the child thread                ("TestCoroutine", "launch:numbersTo50Sum: ${()}")
                delay(1000)
                val naturalNumbers = generateSequence(0) { it + 1 }
                val numbersTo50 =  { it <= 50 }
                ()
            }
            val numbers50To100Sum = withContext() {
               //Execute natural sum of 51-100 in child thread                ("TestCoroutine", "launch:numbers50To100Sum: ${()}")
                delay(1000)
                val naturalNumbers = generateSequence(51) { it + 1 }
                val numbers50To100 =  { it in 51..100 }
                ()
            }
            val result = numbersTo50Sum + numbers50To100Sum
            ("TestCoroutine", "launch end:result=$result ${()}")
        }
        ("TestCoroutine", "Hello World!,${()}")

Console output result:
2023-01-02 16:05:45.846 10153-10153/ D/TestCoroutine: Hello World!,Thread[main,5,main]
2023-01-02 16:05:48.058 10153-10153/ D/TestCoroutine: launch start: Thread[main,5,main]
2023-01-02 16:05:48.059 10153-10322/ D/TestCoroutine: launch:numbersTo50Sum: Thread[DefaultDispatcher-worker-1,5,main]
2023-01-02 16:05:49.114 10153-10322/ D/TestCoroutine: launch:numbers50To100Sum: Thread[DefaultDispatcher-worker-1,5,main]
2023-01-02 16:05:50.376 10153-10153/ D/TestCoroutine: launch end:result=5050 Thread[main,5,main]

In the above code:

  • launchis a function that creates a coroutine and assigns the execution of its function body to the corresponding scheduler.
  • Instructs that this coroutine should be executed on the main thread reserved for UI operations.
  • Indicates that this coroutine should be executed on the thread reserved for I/O operations.
  • withContext()Move the execution of the coroutine to an I/O thread.

From the console output, it can be seen that when calculating the natural sum of 1-50 and 51-100, the thread is from the main thread (Thread[main,5,main]) Switch to the thread of the coroutine (DefaultDispatcher-worker-1,5,main), here 1-50 and 51-100 are calculated as the same child thread.

There is an important phenomenon here. The code seems logically synchronous, and when the coroutine execution task is started, the main thread does not block the main thread and continue to perform related operations. Moreover, after the asynchronous task in the coroutine is completed, it automatically switches back to the main thread. This is the benefit of Kotlin coroutines to development for concurrent programming. This is also a source of concepts: Kotlin coroutine synchronization is non-blocking.

Is synchronous non-blocking really "synchronous non-blocking"? Let's explore the trick below. Through Android Studio, check the above code in the .class file:

      $default((CoroutineScope), (CoroutineContext)(), (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() {
               case 0:
                  ($result);
                  ("TestCoroutine", "launch start: " + ());
                  var6 = (CoroutineContext)();
                  var10001 = (Function2)(new Function2((Continuation)null) {
                     int label;
                     @Nullable
                     public final Object invokeSuspend(@NotNull Object $result) {
                        Object var4 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
                        switch() {
                        case 0:
                           ($result);
                           ("TestCoroutine", "launch:numbersTo50Sum: " + ());
                            = 1;
                           if ((1000L, this) == var4) {
                              return var4;
                           }
                           break;
                        case 1:
                           ($result);
                           break;
                        default:
                           throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
                        }
                        Sequence naturalNumbers = ((0), (Function1));
                        Sequence numbersTo50 = (naturalNumbers, (Function1));
                        return ((numbersTo50));
                     }
                     @NotNull
                     public final Continuation create(@Nullable Object value, @NotNull Continuation completion) {
                        (completion, "completion");
                        Function2 var3 = new <anonymous constructor>(completion);
                        return var3;
                     }
                     public final Object invoke(Object var1, Object var2) {
                        return ((<undefinedtype>)(var1, (Continuation)var2)).invokeSuspend();
                     }
                  });
                   = 1;
                  var10000 = (var6, var10001, this);
                  if (var10000 == var5) {
                     return var5;
                  }
                  break;
               case 1:
                  ($result);
                  var10000 = $result;
                  break;
               case 2:
                  numbersTo50Sum = $0;
                  ($result);
                  var10000 = $result;
                  break label17;
               default:
                  throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
               }
               numbersTo50Sum = ((Number)var10000).intValue();
               var6 = (CoroutineContext)();
               var10001 = (Function2)(new Function2((Continuation)null) {
                  int label;
                  @Nullable
                  public final Object invokeSuspend(@NotNull Object $result) {
                     Object var4 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
                     switch() {
                     case 0:
                        ($result);
                        ("TestCoroutine", "launch:numbers50To100Sum: " + ());
                         = 1;
                        if ((1000L, this) == var4) {
                           return var4;
                        }
                        break;
                     case 1:
                        ($result);
                        break;
                     default:
                        throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
                     }
                     Sequence naturalNumbers = ((51), (Function1));
                     Sequence numbers50To100 = (naturalNumbers, (Function1));
                     return ((numbers50To100));
                  }
                  @NotNull
                  public final Continuation create(@Nullable Object value, @NotNull Continuation completion) {
                     (completion, "completion");
                     Function2 var3 = new <anonymous constructor>(completion);
                     return var3;
                  }
                  public final Object invoke(Object var1, Object var2) {
                     return ((<undefinedtype>)(var1, (Continuation)var2)).invokeSuspend();
                  }
               });
               $0 = numbersTo50Sum;
                = 2;
               var10000 = (var6, var10001, this);
               if (var10000 == var5) {
                  return var5;
               }
            }
            int numbers50To100Sum = ((Number)var10000).intValue();
            int result = numbersTo50Sum + numbers50To100Sum;
            ("TestCoroutine", "launch end:result=" + result + ' ' + ());
            return ;
         }
         @NotNull
         public final Continuation create(@Nullable Object value, @NotNull Continuation completion) {
            (completion, "completion");
            Function2 var3 = new <anonymous constructor>(completion);
            return var3;
         }
         public final Object invoke(Object var1, Object var2) {
            return ((<undefinedtype>)(var1, (Continuation)var2)).invokeSuspend();
         }
      }), 2, (Object)null);
      ("TestCoroutine", "Hello World!," + ());

Although the code in the above .class file is relatively 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 in fact it does not. It’s just that the Kotlin compiler does a lot of things for the code, which is why the Kotlin coroutine is actually a set of thread API frameworks.

Let’s take a look at a variant of the above example:

        () {
            ("TestCoroutine", "launch start: ${()}")
            val numbersTo50Sum = async {
                withContext() {
                    ("TestCoroutine", "launch:numbersTo50Sum: ${()}")
                    delay(2000)
                    val naturalNumbers = generateSequence(0) { it + 1 }
                    val numbersTo50 =  { it &lt;= 50 }
                    ()
                }
            }
            val numbers50To100Sum = async {
                withContext() {
                    ("TestCoroutine", "launch:numbers50To100Sum: ${()}")
                    delay(500)
                    val naturalNumbers = generateSequence(51) { it + 1 }
                    val numbers50To100 =  { it in 51..100 }
                    ()
                }
            }
            // Calculate the natural sum of 1-50 and 51-100 to be two concurrent operations            val result = () + ()
            ("TestCoroutine", "launch end:result=$result ${()}")
        }
        ("TestCoroutine", "Hello World!,${()}")

Console output result:
2023-01-02 16:32:12.637 13303-13303/ D/TestCoroutine: Hello World!,Thread[main,5,main]
2023-01-02 16:32:13.120 13303-13303/ D/TestCoroutine: launch start: Thread[main,5,main]
2023-01-02 16:32:14.852 13303-13444/ D/TestCoroutine: launch:numbersTo50Sum: Thread[DefaultDispatcher-worker-2,5,main]
2023-01-02 16:32:14.853 13303-13443/ D/TestCoroutine: launch:numbers50To100Sum: Thread[DefaultDispatcher-worker-1,5,main]
2023-01-02 16:32:17.462 13303-13303/ D/TestCoroutine: launch end:result=5050 Thread[main,5,main]

asyncA coroutine is created that allows the calculation of the natural sum of 1-50 and 51-100 to be two concurrent operations. The above console output results can be seen that the sum of natural numbers calculated by 1-50 is a threadThread[DefaultDispatcher-worker-2,5,main]whereas calculating the natural sum of 51-100 is in another threadThread[DefaultDispatcher-worker-1,5,main]middle.

From the above example, coroutines are in asynchronous operations, that is, thread switching: the main thread starts the child thread to perform time-consuming operations, and the time-consuming operation is completed and the result is updated to the main thread, the code logic is simplified and readable.

What is suspend

Suspend literal translation is: suspend

suspend is a keyword in the Kotlin language, used to modify methods. When modifying methods, it means that this method can only be called by suspend modified methods or in coroutines.

Let’s take a look at how to split the above code case into several suspend methods:

    fun getNumbersTo100Sum() {
        () {
            ("TestCoroutine", "launch start: ${()}")
            val result = calcNumbers1To100Sum()
            ("TestCoroutine", "launch end:result=$result ${()}")
        }
        ("TestCoroutine", "Hello World!,${()}")
    }
    private suspend fun calcNumbers1To100Sum(): Int {
        return calcNumbersTo50Sum() + calcNumbers50To100Sum()
    }
    private suspend fun calcNumbersTo50Sum(): Int {
        return withContext() {
            ("TestCoroutine", "launch:numbersTo50Sum: ${()}")
            delay(1000)
            val naturalNumbers = generateSequence(0) { it + 1 }
            val numbersTo50 =  { it <= 50 }
            ()
        }
    }
    private suspend fun calcNumbers50To100Sum(): Int {
        return withContext() {
            ("TestCoroutine", "launch:numbers50To100Sum: ${()}")
            delay(1000)
            val naturalNumbers = generateSequence(51) { it + 1 }
            val numbers50To100 =  { it in 51..100 }
            ()
        }
    }

Console output result:
2023-01-03 14:47:57.047 11349-11349/ D/TestCoroutine: Hello World!,Thread[main,5,main]
2023-01-03 14:47:59.311 11349-11349/ D/TestCoroutine: launch start: Thread[main,5,main]
2023-01-03 14:47:59.312 11349-11537/ D/TestCoroutine: launch:numbersTo50Sum: Thread[DefaultDispatcher-worker-3,5,main]
2023-01-03 14:48:00.336 11349-11535/ D/TestCoroutine: launch:numbers50To100Sum: Thread[DefaultDispatcher-worker-1,5,main]
2023-01-03 14:48:01.339 11349-11349/ D/TestCoroutine: launch end:result=5050 Thread[main,5,main]

When the suspend keyword tags a method, it actually tells Kotlin to call the method from within the coroutine. So this "hang" does not mean that the method or function is suspended, nor does it mean that the thread is suspended.

What if a method that is not suspend modified calls a suspend modified method?

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

At this time, 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 view the above method in the .class file calcNumbers50To100Sum code:

   private final Object calcNumbers50To100Sum(Continuation $completion) {
      return ((CoroutineContext)(), (Function2)(new Function2((Continuation)null) {
         int label;
         @Nullable
         public final Object invokeSuspend(@NotNull Object $result) {
            Object var4 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
            switch() {
            case 0:
               ($result);
               ("TestCoroutine", "launch:numbers50To100Sum: " + ());
                = 1;
               if ((1000L, this) == var4) {
                  return var4;
               }
               break;
            case 1:
               ($result);
               break;
            default:
               throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
            }
            Sequence naturalNumbers = ((51), (Function1));
            Sequence numbers50To100 = (naturalNumbers, (Function1));
            return ((numbers50To100));
         }
         @NotNull
         public final Continuation create(@Nullable Object value, @NotNull Continuation completion) {
            (completion, "completion");
            Function2 var3 = new <anonymous constructor>(completion);
            return var3;
         }
         public final Object invoke(Object var1, Object var2) {
            return ((<undefinedtype>)(var1, (Continuation)var2)).invokeSuspend();
         }
      }), $completion);
   }

Can see private suspend fun calcNumbers50To100Sum()After compiling with the Kotlin compiler, it becomesprivate final Object calcNumbers50To100Sum(Continuation $completion)suspendDisappeared, the method has one more parameterContinuation $completion,sosuspendModify Kotlin's method or function, and the compiler will do special treatments for this method.

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

The following is a little modifiedcalcNumbers50To100Summethod:

   private suspend fun calcNumbers50To100Sum(): Int {
        ("TestCoroutine", "launch:numbers50To100Sum:start: ${()}")
        val sum= withContext() {
            ("TestCoroutine", "launch:numbers50To100Sum: ${()}")
            delay(1000)
            val naturalNumbers = generateSequence(51) { it + 1 }
            val numbers50To100 =  { it in 51..100 }
            ()
        }
        ("TestCoroutine", "launch:numbers50To100Sum:end: ${()}")
        return sum
    }

Console output result:
2023-01-03 15:28:04.349 15131-15131/ D/TestCoroutine: Hello World!,Thread[main,5,main]
2023-01-03 15:28:04.803 15131-15131/ D/TestCoroutine: launch start: Thread[main,5,main]
2023-01-03 15:28:04.804 15131-15266/ D/TestCoroutine: launch:numbersTo50Sum: Thread[DefaultDispatcher-worker-3,5,main]
2023-01-03 15:28:06.695 15131-15131/ D/TestCoroutine: launch:numbers50To100Sum:start: Thread[main,5,main]
2023-01-03 15:28:06.696 15131-15131/ D/TestCoroutine: launch:numbers50To100Sum: Thread[main,5,main]
2023-01-03 15:28:07.700 15131-15131/ D/TestCoroutine: launch:numbers50To100Sum:end: Thread[main,5,main]
2023-01-03 15:28:07.700 15131-15131/ D/TestCoroutine: launch end:result=5050 Thread[main,5,main]

The main thread is not affected by coroutine threads.

Summarize

Kotlin coroutines are a set of threading API frameworks. Using it for concurrent programming in the Kotlin locale has more advantages than traditional Thread, Executors and RxJava. The code is logically "synchronized non-blocking", and is simple, easy to read and maintain.

suspendIt is a keyword in the Kotlin language that is used to modify the method. When modifying the method, the method can only besuspendModified methods and coroutine calls. At this time, it also indicates that the method is a time-consuming method, telling the caller that it needs to be used in the coroutine.

Reference documentation:

Kotlin coroutine on Android

Coroutines guide

Next article, will be studiedKotlin Flow

This is the end of this article about Kotlin's exploration of what coroutines are. For more related contents of Kotlin coroutines, please search for my previous articles or continue browsing the related articles below. I hope everyone will support me in the future!