「英」Kotlin 协程最佳实践示例

2,125 阅读4分钟
原文链接: codinginfinite.com

There are so many articles and talks out there which focused on what Kotlin Coroutines are and why, conceptually, they are useful. So, in this article, we’re not gonna see what Coroutines are instead of seeing some practical examples of how to use them…?

Theory without practice is empty; practice without theory is blind.

1. Changing threads in Coroutines

In UI based application like Android, Java Swing etc we need to update the UI only in the main thread and execute the network request in IO thread. Let’s see a simple example first:

launch {
   try{
        val user = repo.fetchUserFromNetwork(userId).await()
        // handle user result
   } catch(e : Exception) {
        // TODO 
   }
}
Recommended for you

When launchlaunch is used without the parameter, it runs in the main thread. In this case, when the above code is executed it executes in the main thread which is not good. So, luckily the coroutines give us the Dispatchers that determine what thread or threads, the corresponding coroutine uses for its execution. In order to execute the above code in an IO thread, we need to add the dispatchers in launchlaunch coroutine builder.

launch(Dispatchers.IO) { 
     try { 
           val user = repo.fetchUserFromNetwork(userId).await() 
           // handle user result 
     } catch(e : Exception) { 
           // TODO 
     } 
}

The IO dispatcher dispatches the network request in a background thread and it allocates additional threads on top of the ones allocated to the Default dispatcher.

Now, let’s say we want to update our UI inside the IO worker thread. With that in mind, the above example becomes:

launch(Dispatchers.IO) {
        try {
            val user = repo.fetchUserFromNetwork(userId).await()
            withContext(Dispatchers.Main) {
                updateUi(user)  // In main thread
            }
            
            // back to IO worker thread.
        } catch (e: Exception) {
            // TODO 
        }
}

The withContextwithContextimmediately shifts execution of the block into different thread inside the block, and back when it completes. Now in our case, you can see that we launched with IO dispatcher and everything in this scope is happening in that IO dispatcher except for the thing in the block where we’ve withContextwithContext. Only the code in the block withContextwithContext is executing in the main thread. Using withContextwithContext fulfills our needs, with a single function call and minimal object allocation, compared to creating a new coroutine with async async or  launchlaunch.

2. Coroutines inner Reference

Coroutines give you the ability while you’re in a coroutine builder to get information back from it. Let’s see an example:

launch(Dispatcher.IO){
   val job = coroutineContext[Job]
   // use the job instance
}

You see we’re getting the instance of a currently executing JobJob instance in the coroutine builder. You can get the JobJob instance in any of existing coroutine builder. Once you’ve JobJob instance you can do things like canceling the job, check its activity or check whether it has children.


3. Coroutines for Debugging

While developing your application you often need to know the name of the coroutine in which your code is executed. However, when coroutine is tied to the processing of the specific request or doing some background specific task, it is better to name it explicitly for debugging purposes.

suspend fun getUserFromNetwork(userId : Int) : User
suspend fun getUserAccountInfo(accountId : Int) : Account

fun log(msg: String) = println("[${Thread.currentThread().name}] $msg")

launch(Dispatchers.IO) {
   val user = async(CoroutineName("userFetchCoroutine")) {
         log("user fetch coroutine start")
         getUserFromNetwork(userId).await()
   }
   withContext(Dispatcher.Main) {
        log("")
        updateUI(user)
   } 
   val account = async(CoroutineName("accountFetchCoroutine")) {
         log("account fetch coroutine start")
         getUserAccountInfo(user.accountId).await()
   }
   // handle user account info
}


Output Of above program
[DefaultDispatcher-worker-1 @userFetchCoroutine#1] user fetch coroutine start
[main]
[DefaultDispatcher-worker-2 @accountFetchCoroutine#1] account fetch coroutine start

You see we’re passing the CoroutineNameCoroutineName context element when initiating the asyncasync coroutine builder. In our above program output, you see it prints the CoroutineNameCoroutineName, attached automatically with the worker id.

Note: The custom coroutine names only be displayed if the debugging mode is turned on. If you’re using IntelliJ go to Edit Configurations -> Configuration and paste the following virtual machine options in VM options: section and click apply.

-Dkotlinx.coroutines.debug

4. Usage of suspending Function

Kotlin programming introduces a concept of suspending function via suspendsuspend modifier. We need to add the suspendsuspend modifier to a function makes it either asynchronous or non-blocking. Let’s take an example to find the Fibonacci Number with suspend suspend function.

fun main(args : Array<String>){
   val fibonacciNumber = getFibonacci(1000000)
   println(fibonacciNumber)
}


private suspend fun getFibonacci(range: Int): BigInteger {
    var first: BigInteger = BigInteger.ZERO
    var second: BigInteger = BigInteger.ONE
    for (i in 1 until range) {
        val sum = first + second
        first = second
        second = sum
        if (i == (range - 1))
            return first
    }
    return BigInteger.ZERO
}

You see the above function takes 10-15 seconds to execute on my machine. Even though by adding the suspend keyword it still blocks the caller thread for quite a long time. Actually, if you write this function in IntelliJ IDEA, then you get “redundant ‘suspend’ modifier” warning, hinting that suspend modifier, by itself, does not magically turn to block functions into non-blocking ones.

So, in order to make this function into non-blocking, we need to implement this convention are provided by withContext coroutine builder. For example, the proper way to turn the getFibonacci into suspending one is:

suspend fun main(args: Array<String>) = coroutineScope {
    val fibonacciNumber = getFibonacci(1000000)
    println(fibonacciNumber)
}

private suspend fun getFibonacci(range: Int) = withContext(Dispatchers.Default) {
    println(Thread.currentThread().name)
    var first: BigInteger = BigInteger.ZERO
    var second: BigInteger = BigInteger.ONE
    for (i in 1 until range) {
        val sum = first + second
        first = second
        second = sum
        if (i == (range - 1))
            return@withContext first
    }
    return@withContext BigInteger.ZERO
}

Now the getFibonacci function launched in main thread without blocking the main thread. Another thing we use is Default dispatcher to execute a CPU-bound task. The default dispatcher is optimized for such CPU-bound functions as it backed by a thread pool.

You can also check out my article on => How kotlin coroutines work with suspend modifier.

5. Attached multiple context Elements

Sometimes we need to define multiple elements for coroutine context. We can use + operator for that. For example, we can launch a coroutine with an explicitly specified dispatcher and an explicitly specified name at the same time.

fun log(msg: String) = println("[${Thread.currentThread().name}] $msg")

fun main() = runBlocking {
    launch(Dispatchers.IO + CoroutineName("MyCustomDispatcher")) {
        log("Hello World")
    }
}

// The output of the above program.
[DefaultDispatcher-worker-1 @MyCustomDispatcher#2] Hello World

The multiple context elements should be useful for debugging purposes.

Overall, I feel like kotlin coroutines give us the experience to work with blocking/non-blocking and suspending/non-suspending in the simplest way. There’s much more yet to learn about how to take the most out of kotlin coroutines. So, if you have some more experience about it, please use the comments to let us know more about it.

If you like what you read please share the knowledge with the community.

Thank you for being here and keep reading…

Related Articles