Hey there! I got a fun puzzle I'm working on for s...
# coroutines
a
Hey there! I got a fun puzzle I'm working on for some educational content. The challenge I'm creating is to find ways to "process shoppers" faster. I came up with two ways to approach it while trying to keep structured concurrency. I'm curious to know which is your personal preference and why - and/or if there are additional suggestions to consider. For me,
checkoutShopper2
ran consistently faster with
heavyWorkForProcessingInventory
Copy code
fun main(): Unit = runBlocking {

    val time = measureTimeMillis {

        val openCheckoutLanes = launch(Dispatchers.Default) {
            val shoppers = listOf(
                async { checkoutShopper2("Jake", 3) },
                async { checkoutShopper2("Zubin", 10) },
                async { checkoutShopper2("Amber", 4) },
                async { checkoutShopper2("Ren", 3) }
            )

            shoppers.awaitAll().forEach {
                println("    $it is checked out!")
            }
        }

        openCheckoutLanes.join()
    }

    println("Shoppers have been checked out. Time: ${time/1000.0} seconds")
}

// average runs from 7 to 25 seconds
private suspend fun checkoutShopper(name: String, numberOfItems: Int): String {
    log("Checking out $name.    ")
    withContext(Dispatchers.Default) {
        println("    $name has $numberOfItems items. Checking out...")
        (1..numberOfItems).forEachIndexed { i, elem ->
            println("        item $elem scanned for $name.")
        }
        // we HAVE to wait for this to wait i.e. represents heavy background work like payments processing
        launch { heavyWorkForProcessingInventory() }.join()
    }
    return name
}

// average runs from 4 to 12 seconds
private suspend fun checkoutShopper2(name: String, numberOfItems: Int): String {
    coroutineScope {
        log("Checking out $name.    ")
        log("    $name has $numberOfItems items. Checking out...")
        (1..numberOfItems).forEachIndexed { i, elem ->
            println("        item $elem scanned for $name.")
        }
        // we HAVE to wait for this to wait i.e. represents heavy background work like payments processing
        heavyWorkForProcessingInventory()
    }
    return name
}

private fun heavyWorkForProcessingInventory(): BigInteger = BigInteger.probablePrime(4096, Random())

// sub out to notice slower runs
private suspend fun heavyWorkForProcessingInventory2(): BigInteger = withContext(Dispatchers.Default) {
    BigInteger.probablePrime(4096, Random())
}
Seems like using
withContext
is surprisingly heavy even if used in the same context, not sure why. It's my understanding that a dispatcher will check if there needs to be a context switch, and if not, then work continues in the same context (according to code comments)
u
First, you should probably make things compareable and post your numbers again. • log vs println is not withContext related • Why do you launch/join in one case and just execute in the other case?
For my preferences, • I would not launch the openCheckoutLanes into any explicit context. Nothing is blocking there • I would only ‘withContext’ blocking work. aka heavyWorkForProcessingInventory. This might hurt performance a bit, as you context-switch for every checkout. Let us know how it goes.
Copy code
fun main(): Unit = runBlocking {

    val time = measureTimeMillis {

            val shoppers = listOf(
                async { checkoutShopper2("Jake", 3) },
                async { checkoutShopper2("Zubin", 10) },
                async { checkoutShopper2("Amber", 4) },
                async { checkoutShopper2("Ren", 3) }
            )

            shoppers.awaitAll().forEach {
                println("    $it is checked out!")
            }
    }

    println("Shoppers have been checked out. Time: ${time/1000.0} seconds")
}

private suspend fun checkoutShopper2(name: String, numberOfItems: Int): String {
        log("Checking out $name.    ")
        log("    $name has $numberOfItems items. Checking out...")
        (1..numberOfItems).forEachIndexed { i, elem ->
            println("        item $elem scanned for $name.")
        }
        // we HAVE to wait for this to wait i.e. represents heavy background work like payments processing
        heavyWorkForProcessingInventory()
    return name
}
private fun heavyWorkForProcessingInventory(): BigInteger = BigInteger.probablePrime(4096, Random()) // sub out to notice slower runs private suspend fun heavyWorkForProcessingInventory2(): BigInteger = withContext(Dispatchers.Default) { BigInteger.probablePrime(4096, Random()) }
1
d
launch { heavyWorkForProcessingInventory() }.join()
This is just a roundabout way of saying
heavyWorkForProcessingInventory()
, there is no benefit to writing it this way at all.
👍 2
a
Hi @uli, thanks for your input! re: your first bullet point. No, it's not related. It's just for recording.
Copy code
fun log(message: String) {
    println("$message    | current thread: ${Thread.currentThread().name}")
}
re: second bullet point. that is a good point out, it should be the case for both for a more accurate comparison. I'll have to read the rest of this thread later tonight and I'll come back with updates!
u
Just make sure if you want to compare something to not have any unrelated differences
💯 1
a
@uli it seems that the first run of
checkoutShopper2
consistent starts lower, suggesting that
withContext
is a bit more expensive using so often. I also notice the time goes up no matter the solution with each repeated run in IntelliJ, I would have to create some scripting and run this code in terminal to see if I get same trend there results or if this behavior more relates with the environment @Dmitry Khalanskiy [JB] thank you for pointing out the lack in difference I got that fixed