Right way to use coroutines


#1

I wrote a simple benchmark comparing coroutines and thread performance on doing IO stuff.

package io.github.tramchamploo

import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.async
import kotlinx.coroutines.experimental.launch
import kotlinx.coroutines.experimental.runBlocking
import org.springframework.jdbc.core.JdbcTemplate
import org.springframework.jdbc.datasource.DriverManagerDataSource
import java.util.*
import java.util.concurrent.*
import kotlin.concurrent.thread
import kotlin.system.measureTimeMillis

/**
 * Created by tramchamploo on 2017/7/6.
 */

class JdbcBenchmark {

    companion object {
        fun propertyOr(key: String, fallback: String) = System.getProperty(key, fallback)

        fun randomString() = ThreadLocalRandom.current().nextLong().toString()
    }

    val dataSource: DriverManagerDataSource by lazy {
        val ds = DriverManagerDataSource()
        ds.setDriverClassName("com.mysql.cj.jdbc.Driver")
        ds.url = propertyOr("jdbcUrl", "jdbc:mysql://192.168.99.100:32767?useSSL=false")
        ds.username = propertyOr("username", "root")
        ds.password = propertyOr("password", "root")
        ds
    }
    val unbatch = JdbcTemplate(dataSource)

    val CREATE_DATABASE = "CREATE DATABASE IF NOT EXISTS test"
    val CREATE_TABLE = "CREATE TABLE test.benchmark(id INT PRIMARY KEY AUTO_INCREMENT, data VARCHAR(32), time TIMESTAMP);"
    val DROP_TABLE = "DROP TABLE IF EXISTS test.benchmark;"
    val INSERTION = "INSERT INTO test.benchmark(data, time) VALUES(?, ?);"

    suspend fun a() {
        unbatch.update(CREATE_DATABASE)
        unbatch.update(DROP_TABLE)
        unbatch.update(CREATE_TABLE)
    }

    suspend fun b() {
        unbatch.update(INSERTION, randomString(), Date())
    }

    fun c() {
        unbatch.update(DROP_TABLE)
        unbatch.update(CREATE_TABLE)
    }

    fun d() {
        unbatch.update(INSERTION, randomString(), Date())
    }
}

fun main(args: Array<String>) {
    val bm = JdbcBenchmark()
    val iterations = 10

    runBlocking {

        val init = async(CommonPool) {
            bm.a()
        }
        init.await()

        val timeUsed = List(iterations) {
            measureTimeMillis {
                val jobs = List(1000) {
                    async(CommonPool) {
                        bm.b()
                    }
                }

                jobs.forEach { it.await() }
            }
        }
        println(timeUsed.average())
    }

    val executor = ForkJoinPool.commonPool()
    bm.c()

    val timeUsed = List(iterations) {
        measureTimeMillis {
            val tasks = List(1000) {
                Callable { bm.d() }
            }
            executor.invokeAll(tasks)
        }
    }
    println(timeUsed.average())
    executor.shutdown()
}

Run result:

4224.7
3367.1

And it turned out that coroutines are about a quarter slower than threads. Isn’t it coroutines perfect for I/O blocking operations like jdbc. What am I wrong about usage on coroutines?


#2

In the second test there is no joining of tasks. You start them but do not wait for them to finish.
As for courutines (I am an neophyte myself), I believe is that the greatest advantage will be that you get explicit result of futures in a non blocking way. In your example you will have to call blocking join to obtain them. The internal mechanics in both of your examples is precisely the same since in this case coroutines are backed by commonPool.


#3

Well, I believe that executor.invokeAll waits until finish. Otherwise the second test won’t cost that much.
In my opinion, the update operation will suspend when communicating with database server, so if I use a dispatcher with less thread on coroutines, it will still work fast, am I right?


#4

Hi @tramchamploo,
friendly: you simply made a database benchmark, and you made it wrong.

The fun a and the fun c are different, so results are not comparable.

The fun c is right after the `fun a, so I can suppose that the database is entirely in the ram disk buffer, so the result is not comparable.

The fun a and fun b start using a cold JVM, so everything run later it will run faster.

CommonPool and ForkJoinPool.commonPool have different number of thread, so parallelism is different and the result is not comparable.

Finally you must not use a cpu bounded thread pool for blocking operations, your 386 can send hundred of SQL update without any issue, so use a different thread pool, but you should never use CommonPool or ForkJoinPool.commonPool.


#5

@fvasco So what I’m confused about is whether coroutines can improve throughput of an application full of blocking I/O operations like JDBC.

You mentioned about I should use another thread pool for JDBC, does this mean coroutines won’t decrease the number of threads I use? As we all know JDBC doesn’t have async api, so can I benefit from coroutines in this scenario? If i can, what am I supposed to do?


#6

Yes, you are right. At least according to documentation. Sadly I am not able to run test myself right now. But I think that in order coroutines you should start with artificial tests. DB access complicates things.


#7

Kotlin is a programming language, so as a language it offers you a different syntax for asynchronous programming: this is the key feature for coroutine.

coroutines can improve throughput of an application full of blocking I/O operations

No, multithread programming can do by parallelizing blocking operations.
You should use coroutine but this is only a syntax switch.

You mentioned about I should use another thread pool for JDBC, does this mean coroutines won’t decrease the number of threads I use?

No, at least not automagically (like Quasar does).

As we all know JDBC doesn’t have async api, so can I benefit from coroutines in this scenario?


#8

So now I know it’s just a syntax switch. So if I write my api with kotlin in suspend style, can I use it in java?


#9

No, you can’t.
You have to use “callback style”.


#10

One last question, is it meaningful of running a coroutine with no suspend functions?


#11

Coroutines work best when you do use suspending functions. Without suspending functions, threads work just as well as coroutines. However, if you are looking for consistency and want to have coroutines-only code without any mix, then it is perfectly Ok to use coroutines even for the code that does not do any suspension.


#12

Thank you guys! This solves my confusion.


#13

So if I use in my routine JDBC Thrift HTTP connects etc. can I use coroutines and will it works fast? Does co-routine threads will wait for end of this IO operations or it not blocks ?