Small knowledge, big challenge! This article is participating in the creation activity of “Essential Tips for Programmers”

This article also participated in the “Digitalstar Project” to win a creative gift package and creative incentive money

📕 What is a coroutine?

Official description: Coroutines simplify asynchronous programming by putting complexity into libraries. The program logic can be expressed sequentially in coroutines, and the underlying library takes care of the asynchrony for us. The library can wrap the relevant parts of user code as callbacks, subscribing to related events, and scheduling execution on different threads (or even different machines), while keeping the code as simple as sequential execution.

Coroutines are like very lightweight threads

Coroutines are a concurrent design pattern that you can use on the Android platform to simplify code that executes asynchronously. Coroutines were added to Kotlin in version 1.3 and are based on established concepts from other languages.

Coroutines origin

“Coroutines” comes from Simula and Modula-2. The term was coined by Melvin Edward Conway as early as 1958 to build assembler programs, illustrating that Coroutines are a programming idea that is not limited to a particular language.

Which languages are used for coroutines?

  • The Lua language

    Lua uses coroutines starting with version 5.0, implemented through the extension library Coroutine.

  • The Python language

    As in the code examples just written, Python can implement coroutines in a yield/send manner. After Python 3.5 async/await becomes a better alternative.

  • The Go

    The Go language’s implementation of coroutines is very powerful and concise, making it easy to create hundreds or thousands of coroutines and execute them concurrently.

  • The Java language

    As mentioned above, there is no native support for coroutines in the Java language, but some open source frameworks simulate coroutines. If you are interested, take a look at the source code of Kilim framework:

github.com/kilim/kilim

Coroutines advantages

  • Lightweight: You can run multiple coroutines on a single thread because coroutines support suspension and do not block the thread running them. ** Suspension saves memory than blocking and supports multiple parallel operations.

  • Fewer memory leaks: Multiple operations are performed within a scope using structured concurrency mechanisms.

  • Built-in cancel support: Cancel operations are automatically propagated throughout the running coroutine hierarchy.

  • Jetpack Integration: Many Jetpack libraries include extensions that provide full coroutine support. Some libraries also provide their own coroutine scope that you can use to structure concurrency.

🌴 For example:

Asynchronous code & callback hell

For example, if we have a request, we make an asynchronous request, which queries the user information from the server, and returns a response via CallBack:

        getUserInfo(new CallBack()) {
            @Override
            public Void onSuccess (String response){
                if(response ! =null) { System.out.println(response); }}}Copy the code

If the requirements are now modified, query the user’s information → find the user’s friends list → find the friends’ activity (the code looks very bloated)

Graph TD queries user information --> find the list of friends of that user --> Find the activity of that friend
// Query user information
getUserInfo(new CallBack() {
    @Override
    public Void onSuccess(String user) {
        if(user ! =null) {
            System.out.println(user);
            // Find the user's friends list
            getFriendList(user, new CallBack() {
                @Override
                public Void onSuccess(String friendList) {
                    if(friendList ! =null) {
                        System.out.println(friendList);
                        // Find the friend's activity
                        getFeedList(friendList, new CallBack() {
                            @Override
                            public Void onSuccess(String feed) { System.out.println(feed); }}); }}); }}}});Copy the code

Hell to Heaven (using Coroutines)

val user = getUserInfo()
val friendList = getFriendList(user)
val feedList = getFeedList(friendList)
Copy the code

Is it super simple? That’s the beauty of Kotlin coroutines: asynchronous tasks are done in a synchronous manner.

To better understand, before using coroutines, let’s talk about threads and processes

What are processes and threads?

What is the process?

In plain English, a process is a startup instance of an application. For example, when we run a game or open a piece of software, we start a process. Processes have code and open file resources, data resources, and separate memory space.

What about threads?

The thread is subordinate to the process and is the actual executor of the program. A process contains at least one main thread and may have more child threads. Threads have their own stack space.

To an operating system, a thread is the smallest unit of execution and a process is the smallest unit of resource management. Both processes and threads are managed by the operating system.

Threads have five states in Java

Who does the transition between thread states? Is that the JVM?

And it isn’t. The JVM uses the TCB (Thread Control Block) module in the operating system kernel to change the state of threads, which consumes CPU resources. In Java API, Thread class is the most basic class to implement threads. Each creation of a Thread object represents the start of a Thread in the operating system kernel. If we read the source code of Thread class, we can find that its internal implementation is a large number of JNI calls. Because the implementation of threads must be directly supported by the operating system.

Relationship between threads and coroutines

  • Lightweight and efficient

    Threads are scheduled by the system, and the overhead of thread switching or thread blocking is high

  • Thread dependent

    Cannot run off thread

  • Threads are not blocked when suspended

    The coroutine base library also handles blocking tasks asynchronously, but these complex operations are encapsulated by the base library, and the program flow of the coroutine code is sequential

  • A thread can create any coroutine

  • Switching between different threads

  • Coroutines controllable

    The coroutine base library also handles blocking tasks asynchronously, but these complex operations are encapsulated by the base library, and the program flow of the coroutine code is sequential

🌼Coroutines is available

Environment to prepare

Kotlin coroutines is used in Android, Kotlin1.3 or later, you need to introduce the Kotlinx. coroutines library:

implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:x.x.x'//1
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:x.x.x'
Copy the code

Since coroutines were officially supported in version 1.3 of Kotlin, the Kotlinx-Coroutines-Core library has been integrated into the Kotlin language package. Package size:

  • Kotlinx coroutines — core: 0.9 M

  • Kotlinx coroutines – android: 21 k

So if we introduce the Kotlinx-Coroutines-Android library into our current Android project using Kotlin, the package size will increase by a negligible 21K.

Several ways to create coroutines

way role
launch:Job To create aDoes not blockThe current thread, Coroutine that does not return a result, but does return oneJobObject, which controls the execution and cancellation of this Coroutine
runBlocking:T To create ablocksThe Coroutine of the current thread, often used in unit testing scenarios, is not typically used in development
async/awit:Deferred Async returns oneDeferredInterface,Deferred interface inheritance and Job

Launch :Job(Most commonly used, not blocking)

fun main(a) {
    println("Main thread started")
    
    // Scope can be used with GlobalScope(not recommended), lifecycleScope, viewModelScope, etc.
    GlobalScope is used here for demonstration purposes only
    val job = GlobalScope.launch {// Return the Job object
        repeat(5) {
            println("Launch coroutine:" + Thread.currentThread())
            delay(1000)
        }
    }

    println("End of main thread")
    Sleep only keeps the process alive in order to wait for the coroutine to complete
    Thread.sleep(8000L)}Copy the code

Execute the main function and print the result:

The result shows that the current thread is not blocked (the coroutine continues to print logs after the main thread ends)

Main Thread Start Main Thread end launch coroutine: Thread[DefaultDispatcher-worker-1.5,main]
launch协程:Thread[DefaultDispatcher-worker-1.5,main]
launch协程:Thread[DefaultDispatcher-worker-1.5,main]
launch协程:Thread[DefaultDispatcher-worker-1.5,main]
launch协程:Thread[DefaultDispatcher-worker-1.5,main]

Process finished with exit code 0
Copy the code

👉 use different scope need to add corresponding extension function

/ / use lifecycleScope
implementation 'androidx. Lifecycle: lifecycle - runtime - KTX: 2.2.0'
/ / use viewModelScope
implementation 'androidx. Lifecycle: lifecycle - viewmodel - KTX: 2.2.0'
Copy the code

Use method 2 :runBlocking(blocking)

fun main(a) {
    println("Main thread started")

    val any = runBlocking {
        repeat(5) {
            println("RunBlocking coroutine:" + Thread.currentThread())
            delay(1000)
        }
    }

    println("End of main thread")
    Sleep only keeps the process alive in order to wait for the coroutine to complete
    //Thread.sleep(8000L)
}
Copy the code

Execute the main function and print the result:

The result shows that the current thread is blocked (the main thread ends after the coroutine prints the log)

RunBlocking coroutine: Thread[main,5,main] runBlocking coroutine: Thread[main,5,main] runBlocking coroutine: Thread[main,5,main] runBlocking coroutine: Thread[main,5,main] runBlocking coroutine: Thread[main,5,main] The main thread endsCopy the code

Async /await:Deferred(Don't block)

fun main(a) {
    println("Main thread started")

    GlobalScope.launch {
        // Start time
        val startTime = System.currentTimeMillis()

        // Simulate asynchronous fetch request 1
        val result1 = async {
            getResult1()
        }
        // Simulate asynchronous fetch request 2
        val result2 = async {
            getResult2()
        }
        // merge 2 to request results
        val result = result1.await() + "+" + result2.await()
        println("result =    $result")

        // End time
        val endTime = System.currentTimeMillis()

        println("time =    ${endTime - startTime}")
    }

    println("End of main thread")
    Sleep only keeps the process alive in order to wait for the coroutine to complete
    Thread.sleep(8000L)}// Delay 3s to get results
suspend fun getResult1(a): String {
    delay(3000)
    return "result1"
}

// Delay getting results by 4s
suspend fun getResult2(a): String {
    delay(4000)
    return "result2"
}
Copy the code

Execute the main function and print the result:

The result shows that the current thread is not blocked, saving the request time by using async (3s+4s normally, 4s in real time).

Result = result1+result2 time =4010

Process finished with exit code 0
Copy the code