Asynchronous programming is gaining momentum because it allows much more efficient use of processor time. Thus, the application can handle more requests per unit of time and is better scaled.
Today there are several options for implementing asynchronous approaches:
Coroutines allow you to keep the application code in its usual form, performing all the operations of switching between tasks under the hood.
suspend fun doSomething(arg: Argument) {
val firstValue = sendRequest(arg)
val secondValue = sendAnotherRequest(firstValue)
}
In the example, we mark the doSomething function with the suspend keyword, which means that the answer to it will not come immediately. The function itself also executes some asynchronous requests one after another.
The suspend function cannot be called from a regular function, because it is unclear for the JVM on which thread it should be executed.
If we want to execute a function on the same thread, we can use runBlocking. Of course, this option will not allow us to efficiently utilize CPU resources. But it can be used in tests or command line interfaces.
fun casualFunction() {
runBlocking { suspendFunction() }
}
fun suspend suspendFunction() {
doSomething()
}
Another way is to use a separate thread pool. The example below uses Dispatchers.IO, which defaults to max(64, number of cores), but you can set a custom thread pool.
fun casualFunction() {
CoroutineScope(Dispatchers.IO).launch {
delay(1000)
println("Launch finished")
}
println("Casual function finished")
}
It is important to understand that the call to the launch block will occur in a different thread, and the method will continue its execution, so “Casual function finished” will be printed out first.
What if we need to run a heavy function (e.g. IO operation) inside suspend function? If we just run it, it will block the current thread, and it can ruin the whole idea of async programming.
Instead, we can define a separate context for slow operations so as not to block the existing threads.
suspend fun suspendFunction() {
val context = Executors.newFixedThreadPool(10).asCoroutineDispatcher()
withContext(context) {
runSomethingSlow()
}
}
Now, an outer dispatcher will suspend and delegate the control to our custom context. When it is finished, the custom context will return the control to the outer dispatcher.
That is pretty much it to start using coroutines. You can find the complete documentation here.