正文
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 launch 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 launch 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 withContextimmediately 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 withContext. Only the code in the block withContext is executing in the main thread. Using withContext fulfills our needs, with a single function call
and minimal object allocation, compared to creating a new coroutine with async or launch.
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 Job instance in the coroutine builder. You can get the Job instance in any of existing coroutine builder. Once you’ve Job 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 CoroutineName context element when initiating the async coroutine builder. In our above program output, you see it prints the CoroutineName, 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 suspend modifier. We need to add the suspend modifier to a function makes it either asynchronous or non-blocking. Let’s take an example to find the
Fibonacci Number
with 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)
-
}
-