/ COROUTINE, CONCURRENT PROGRAMMING, THREAD

Even and odd with coroutines

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")
}
1Since the function is ran in a coroutine context, the suspend modifier must be used
2Get the value wrapped inside the atomic integer. This is not thread-safe, as the value can be updated just after the call
3Simulate a long-running operation and returns the thread to the pool
4Check the coroutine has the right parity. If not, just print it hasn’t
5If 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.
6Create a thread pool with 5 threads. Any number of threads can be used safely
7Create the AtomicInteger instance that will be shared in functions. It manages the lock
8The measureTimeMillis is a very useful utility function to measure the time elapsed (obviously)
9The launch() function runs the lambda block within the designated scope. Here, the global scope with the thread pool created above is used underneath
10Launch new coroutines until the desired limit is reached
11Coroutines need to be run into a dedicated coroutine scope. runBlocking creates such a scope
12Wait 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.

Nicolas Fränkel

Nicolas Fränkel

Nicolas Fränkel is a Developer Advocate with 15+ years experience consulting for many different customers, in a wide range of contexts (such as telecoms, banking, insurances, large retail and public sector). Usually working on Java/Java EE and Spring technologies, but with focused interests like Rich Internet Applications, Testing, CI/CD and DevOps. Currently working for Exoscale. Also double as a teacher in universities and higher education schools, a trainer and triples as a book author.

Read More