Kdo dění kolem Kotlinu alespoň trochu sleduje, určitě již narazil na pojem „coroutines“. Už jsem se na ně chystal nějakou dobu a nyní konečně nastal okamžik, kdy jsem si našel čas si je na vlastní kůži vyzkoušet a opět prozkoumat, jak moc jsou vlastně coroutines použitelné v praxi.

Kód

Jelikož je stručnost mé druhé jméno, bez dalších zbytečných slov si rovnou do POMka šoupněte:

    <repositories>
        <repository>
            <id>jcenter</id>
            <url>https://jcenter.bintray.com/</url>
        </repository>
    </repositories>

    <dependencies>
        <dependency>
            <groupId>org.jetbrains.kotlinx</groupId>
            <artifactId>kotlinx-coroutines-core</artifactId>
            <version>0.19</version>
        </dependency>
        <dependency>
            <groupId>com.github.kittinunf.fuel</groupId>
            <artifactId>fuel</artifactId>
            <version>1.11.0</version>
        </dependency>
    </dependencies>

a ve svém oblíbeném IDE si vytvořte tento test:

package coroutinestest

import com.github.kittinunf.fuel.Fuel
import kotlinx.coroutines.experimental.CoroutineDispatcher
import kotlinx.coroutines.experimental.asCoroutineDispatcher
import kotlinx.coroutines.experimental.async
import kotlinx.coroutines.experimental.runBlocking
import org.junit.After
import org.junit.Before
import org.junit.Test
import java.util.concurrent.Callable
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors

class CoroutinesTest {

    private val url = "http://www.whatismyip.com"

    private val reqestsCount = 20
    private val poolSize = 4

    private lateinit var executorService: ExecutorService
    private lateinit var pool: CoroutineDispatcher

    @Before
    fun setUp() {
        executorService = Executors.newFixedThreadPool(poolSize)
        pool = executorService.asCoroutineDispatcher()
    }

    @After
    fun tearDown() {
        executorService.shutdown()
    }

    fun fetchContent() = Fuel.get(url).responseString().third.get()

    suspend fun suspendableFetchContent() = fetchContent()

    @Test
    fun testSingleThread() {
        val charactersCount = (1..reqestsCount).map { fetchContent() }
                .map { it.length }
                .sum()

        println("$charactersCount (Single-threaded)")
    }

    @Test
    fun testMultiThread() {
        val charactersCount = (1..reqestsCount).map {
            executorService.submit(Callable { fetchContent() })
        }
        .map { it.get() }
        .map { it.length }
        .sum()

        println("$charactersCount (Multi-threaded)")
    }

    @Test
    fun testCoroutinesSuspend() {
        runBlocking {
            val charactersCount = (1..reqestsCount).map {
                suspendableFetchContent()
            }
            .map { it.length }
            .sum()

            println("$charactersCount (Coroutines suspend)")
        }
    }

    @Test
    fun testCoroutinesAsync() {
        runBlocking {
            val charactersCount = (1..reqestsCount).map {
                async(pool) { fetchContent() }
            }
            .map { it.await() }
            .map { it.length }
            .sum()

            println("$charactersCount (Coroutines async)")
        }
    }
}

Výsledky

Po prvním běhu dostanu tyto výsledky:

CoroutinesTest.testSingleThread – 21.1 s
CoroutinesTest.testMultiThread – 9.1 s
CoroutinesTest.testCoroutinesAsync – 7.3 s
CoroutinesTest.testCoroutinesSuspend – 21.9 s

Zde jsem záměrně zvolil trochu pomalejší web. Když zkusím např. google.com, dostanu výsledky následující:

CoroutinesTest.testSingleThread – 3.1 s
CoroutinesTest.testMultiThread – 1.7 s
CoroutinesTest.testCoroutinesAsync – 0.8 s
CoroutinesTest.testCoroutinesSuspend – 2.7 s

Vyvození

Když se dívám na výsledky těchto testů, jsem trochu zklamaný. Říkal jsem si, jestli opravdu dokáže vícero corutin běžet na jednom threadu, ale jak se zdá, pokud volám operace, které thread blokují, což jsou v případě Javy a IO operací asi úplně všechny, pak je to asi opravdu 1 corutina – 1 vlákno. Jediná úspora jde vidět v režii spuštění úlohy pomoci executorService a corutiny v případě rychlejšího webu (Google). Tzn. pokud voláte velké množství úloh na jednom thread poolu, kde každá z nich trvá relativně krátkou dobu (2s nebo 1s a méně), pak vám nejspíše corutiny ušetří opravdu hodně (režie). Avšak pokud každá trvá podstatně déle, rozdíl oproti klasickým threadům bude nejspíše zanedbatelný.

Podobně jsem zklamán z klíčového slova suspend, které by mělo být vlastně to samé, co async { … }.await(). Tzn. zde spustíme jednu úlohu a ihned čekáme na výsledek. Pokud je operace uvnitř této funkce blokující, tak daná metoda, bez ohledu na to, že je označena jako suspendující, blokuje a je srovnatelná se single-thread řešením.

Závěr

K čemu jsou potom vlastně coroutines dobré? Určitě jsou fajn v tom, že namísto mnoha za sebou navazujících callbacků můžeme psát kód ve více „synchronním“ stylu, ale úplně to samé se dá dle mého názoru docílit i s běžnými Javovskýmí Future(s). Pak je fajn, že režie spuštěni corutiny je znatelně menší než u threadu, avšak to se projeví pouze u aplikací, kde hraje propustnost nějakou významnou roli (osobně jsem takovou ještě nepsal). No a pokud ve svých metodách blokujete, klíčové slovo suspend je vlastně úplně k ničemu – vždy budete chtít async {} a na výsledek si počkat až někdy později.

Nicméně pořád mi vrtá hlavou, budeme např. někdy schopni poslat současně 100 HTTP requestů v jediném threadu? Já myslím, že ano, ale pro tohle bude zřejmě potřeba přepsat všechny Javovské IO knihovny z blokujícího do suspendujícího stylu a to si myslím, že je jedna z dalších logických a možných cest, kam by se mohl svět programování ubírat v následujících 5 letech (zde pak klíčové slovo suspend nabere opět svůj význam).