Recently, I stumbled upon one of Baeldung’s post showing how to use threads to print odd and even numbers: one thread dedicated to print odd numbers, another one to print even ones.
Since I became aware of them, I was very interested in Kotlin coroutines and how they make concurrent programming code easier to read and write. I wanted to check how using coroutines would yield better code.
Show me the code
This is what I came up with:
import java.util.concurrent.ThreadLocalRandom
import java.util.concurrent.atomic.AtomicInteger
import kotlin.system.measureTimeMillis
import kotlinx.coroutines.*
enum class Parity(private val label: String, private val mod: Int) {
EVEN("Even", 0), ODD("Odd ", 1);
private val random = ThreadLocalRandom.current()
suspend fun updateIfMatch(count: AtomicInteger) { (1)
val value = count.get() (2)
val duration = random.nextInt(1000).toLong()
delay(duration) (3)
if (mod == value % 2) { (4)
if (count.compareAndSet(value, value + 1)) { (5)
println("[${Thread.currentThread().name}] ${label}: Set to $value")
} else println("[${Thread.currentThread().name}] ${label}: Missed my turn, doing nothing")
} else println("[${Thread.currentThread().name}] ${label}: Not my turn, doing nothing")
}
}
fun main(args: Array<String>) {
val limit = 40
val context = newFixedThreadPoolContext(5, "Deadpool") (6)
val count = AtomicInteger(0) (7)
val time = measureTimeMillis { (8)
val odds = GlobalScope.launch(context) { (9)
while (count.get() < limit) { (10)
Parity.ODD.updateIfMatch(count)
}
}
val evens = GlobalScope.launch(context) { (9)
while (count.get() < limit) { (10)
Parity.EVEN.updateIfMatch(count)
}
}
runBlocking { (11)
odds.join() (12)
evens.join() (12)
}
}
println("Run even/odd in $time ms")
}
1 | Since the function is ran in a coroutine context, the suspend modifier must be used |
2 | Get the value wrapped inside the atomic integer. This is not thread-safe, as the value can be updated just after the call |
3 | Simulate a long-running operation and returns the thread to the pool |
4 | Check the coroutine has the right parity. If not, just print it hasn’t |
5 | If the value hasn’t be updated by the other method, increment it atomically - this call is actually thread-safe. If it has been updated, just print. |
6 | Create a thread pool with 5 threads. Any number of threads can be used safely |
7 | Create the AtomicInteger instance that will be shared in functions.
It manages the lock |
8 | The measureTimeMillis is a very useful utility function to measure the time elapsed (obviously) |
9 | The launch() function runs the lambda block within the designated scope.
Here, the global scope with the thread pool created above is used underneath |
10 | Launch new coroutines until the desired limit is reached |
11 | Coroutines need to be run into a dedicated coroutine scope.
runBlocking creates such a scope |
12 | Wait for completion of the jobs created above |
Comparison with Baeldung’s version
There are several differences with the Java version:
- Thread vs coroutines
-
Obviously, the biggest difference is the usage of couroutines. The Java version "binds" one thread to even numbers writing, and another one to odd numbers writing. One of the main advantage of coroutines is that the number of threads in the thread pool can be changed very easily. Even keeping the even/odd parity only, the number of threads can be increased with the same output. It’s also a no-brainer to change the code to change the step to 3 (or any other number).
- Locking approach
-
Baeldung’s shows two different ways: the legacy
wait()
/notify()
and the semaphor. I prefer to use an atomic object. It has a couple of advantages:- it doesn’t require any additional code
- its semantics is pretty clear
- the lock is managed by the object itself, so that the reasoning about it is quite straightforward
Conclusion
There’s no such thing as a free lunch. Coroutines makes things easier, but they are no magic. In particular, the context in which they run has to be chosen very carefully. However, they just up the ante toward making asynchronous code more accessible for developers to reason about.