Now that we have covered the basics of concurrency, it's a good time to discuss the specifics of concurrency in Kotlin. This section will showcase the most differentiating characteristics of Kotlin when it comes to concurrent programming, covering both philosophical and technical topics.
Concurrency in Kotlin
Non-blocking
Threads are heavy, expensive to create, and limited—only so many threads can be created—So when a thread is blocked it is, in a way, being wasted. Because of this, Kotlin offers what is called Suspendable Computations; these are computations that can suspend their execution without blocking the thread of execution. So instead of, for example, blocking thread X to wait for an operation to be made in a thread Y, it's possible to suspend the code that has to wait and use thread X for other computations in the meantime.
Furthermore, Kotlin offers great primitives like channels, actors, and mutual exclusions, which provide mechanisms to communicate and synchronize concurrent code effectively without having to block a thread.
Being explicit
Concurrency needs to be thought about and designed for, and because of that, it's important to make it explicit in terms of when a computation should run concurrently. Suspendable computations will run sequentially by default. Since they don't block the thread when suspended, there's no direct drawback:
fun main(args: Array<String>) = runBlocking {
val time = measureTimeMillis {
val name = getName()
val lastName = getLastName()
println("Hello, $name $lastName")
}
println("Execution took $time ms")
}
suspend fun getName(): String {
delay(1000)
return "Susan"
}
suspend fun getLastName(): String {
delay(1000)
return "Calvin"
}
In this code, main() executes the suspendable computations getName() and getLastName() in the current thread, sequentially.
Executing main() will print the following:
This is convenient because it's possible to write non-concurrent code that doesn't block the thread of execution. But after some time and analysis, it becomes clear that it doesn't make sense to have getLastName() wait until after getName() has been executed since the computation of the latter has no dependency on the former. It's better to make it concurrent:
fun main(args: Array<String>) = runBlocking {
val time = measureTimeMillis {
val name = async { getName() }
val lastName = async { getLastName() }
println("Hello, ${name.await()} ${lastName.await()}")
}
println("Execution took $time ms")
}
Now, by calling async {...} it's clear that both of them should run concurrently, and by calling await() it's requested that main() is suspended until both computations have a result:
Readable
Concurrent code in Kotlin is as readable as sequential code. One of the many problems with concurrency in other languages like Java is that often it's difficult to read, understand, and/or debug concurrent code. Kotlin's approach allows for idiomatic concurrent code:
suspend fun getProfile(id: Int) {
val basicUserInfo = asyncGetUserInfo(id)
val contactInfo = asyncGetContactInfo(id)
createProfile(basicUserInfo.await(), contactInfo.await())
}
This suspend method calls two methods that will be executed in background threads and waits for their completion before processing the information. Reading and debugging this code is as simple as it would be for sequential code.
Leveraged
Creating and managing threads is one of the difficult parts of writing concurrent code in many languages. As seen before, it's important to know when to create a thread, and almost as important to know how many threads are optimal. It's also important to have threads dedicated to I/O operations, while also having threads to tackle CPU-bound operations. And communicating/syncing threads is a challenge in itself.
Kotlin has high-level functions and primitives that make it easier to implement concurrent code:
- To create a thread it's enough to call newSingleThreadContext(), a function that only takes the name of the thread. Once created, that thread can be used to run as many coroutines as needed.
- Creating a pool of threads is as easy, by calling newFixedThreadPoolContext() with the size and the name of the pool.
- CommonPool is a pool of threads optimal for CPU-bound operations. Its maximum size is the amount of cores in the machine minus one.
- The runtime will take charge of moving a coroutine to a different thread when needed .
- There are many primitives and techniques to communicate and synchronize coroutines, such as channels, mutexes, and thread confinement.
Flexible
Kotlin offers many different primitives that allow for simple-yet-flexible concurrency. You will find that there are many ways to do concurrent programming in Kotlin. Here is a list of some of the topics we will look at throughout the book:
- Channels: Pipes that can be used to safely send and receive data between coroutines.
- Worker pools: A pool of coroutines that can be used to divide the processing of a set of operations in many threads.
- Actors: A wrapper around a state that uses channels and coroutines as a mechanism to offer the safe modification of a state from many different threads.
- Mutual exclusions (Mutexes): A synchronization mechanism that allows the definition of a critical zone so that only one thread can execute at a time. Any coroutine trying to access the critical zone will be suspended until the previous coroutine leaves.
- Thread confinement: The ability to limit the execution of a coroutine so that it always happens in a specified thread.
- Generators (Iterators and sequences): Data sources that can produce information on demand and be suspended when no new information is required.
All of these are tools that are at your fingertips when writing concurrent code in Kotlin, and their scope and use will help you to make the right choices when implementing concurrent code.