Introduction aux Coroutines Kotlin

Introduction aux coroutines de Kotlin

1. Vue d'ensemble

Dans cet article, nous allons examiner les coroutines du langage Kotlin. En termes simples,coroutines allow us to create asynchronous programs in a very fluent way, et ils sont basés sur le concept de programmationContinuation-passing style.

Le langage Kotlin nous donne des constructions de base mais peut accéder à des coroutines plus utiles avec la bibliothèquekotlinx-coroutines-core. Nous examinerons cette bibliothèque une fois que nous aurons compris les éléments de base du langage Kotlin.

2. Créer une Coroutine avecBuildSequence

Créons une première coroutine en utilisant la fonctionbuildSequence.

Et implémentons un générateur de séquence de Fibonacci en utilisant cette fonction:

val fibonacciSeq = buildSequence {
    var a = 0
    var b = 1

    yield(1)

    while (true) {
        yield(a + b)

        val tmp = a + b
        a = b
        b = tmp
    }
}

La signature d'une fonctionyield est:

public abstract suspend fun yield(value: T)

Le mot clésuspend signifie que cette fonction peut être bloquante. Une telle fonction peut suspendre une coroutine debuildSequence.

Suspending functions can be created as standard Kotlin functions, but we need to be aware that we can only call them from within a coroutine. Sinon, nous obtiendrons une erreur de compilation.

Si nous avons suspendu l'appel dans lesbuildSequence,, cet appel sera transformé en état dédié dans la machine à états. Une coroutine peut être passée et assignée à une variable comme n'importe quelle autre fonction.

Dans la coroutinefibonacciSeq, nous avons deux points de suspension. Premièrement, lorsque nous appelonsyield(1) et ensuite lorsque nous appelonsyield(a+b).

Si cette fonctionyield entraîne un appel de blocage, le thread actuel ne s'y bloquera pas. Il sera capable d'exécuter un autre code. Une fois que la fonction suspendue a terminé son exécution, le thread peut reprendre l'exécution de la coroutine defibonacciSeq.

Nous pouvons tester notre code en prenant quelques éléments de la séquence de Fibonacci:

val res = fibonacciSeq
  .take(5)
  .toList()

assertEquals(res, listOf(1, 1, 2, 3, 5))

3. Ajout de la dépendance Maven pourkotlinx-coroutines

Regardons la bibliothèquekotlinx-coroutines qui a des constructions utiles construites au-dessus des coroutines de base.

Ajoutons la dépendance à la bibliothèquekotlinx-coroutines-core. Notez que nous devons également ajouter le référentieljcenter:


    org.jetbrains.kotlinx
    kotlinx-coroutines-core
    0.16



    
        central
        http://jcenter.bintray.com
     

4. Programmation asynchrone à l'aide de la soroutinelaunch() C

La bibliothèquekotlinx-coroutines ajoute de nombreuses constructions utiles qui nous permettent de créer des programmes asynchrones. Supposons que nous ayons une fonction de calcul coûteuse qui ajoute unString à la liste d'entrée:

suspend fun expensiveComputation(res: MutableList) {
    delay(1000L)
    res.add("word!")
}

Nous pouvons utiliser une coroutinelaunch qui exécutera cette fonction de suspension de manière non bloquante - nous devons lui passer un pool de threads comme argument.

La fonctionlaunch renvoie une instanceJob sur laquelle on peut appeler une méthodejoin() pour attendre les résultats:

@Test
fun givenAsyncCoroutine_whenStartIt_thenShouldExecuteItInTheAsyncWay() {
    // given
    val res = mutableListOf()

    // when
    runBlocking {
        val promise = launch(CommonPool) {
          expensiveComputation(res)
        }
        res.add("Hello,")
        promise.join()
    }

    // then
    assertEquals(res, listOf("Hello,", "word!"))
}

Pour pouvoir tester notre code, nous passons toute la logique dans la coroutine derunBlocking- qui est un appel bloquant. Par conséquent, nosassertEquals() peuvent être exécutés de manière synchrone après le code à l'intérieur de la méthoderunBlocking().

Notez que dans cet exemple, bien que la méthodelaunch() soit déclenchée en premier, il s'agit d'un calcul retardé. Le thread principal continuera en ajoutant les“Hello,” String à la liste de résultats.

Après le délai d'une seconde introduit dans la fonctionexpensiveComputation(), les“word!” String seront ajoutés au résultat.

5. Les coroutines sont très légères

Imaginons une situation dans laquelle nous voulons effectuer 100 000 opérations de manière asynchrone. Créer un nombre aussi élevé de threads sera très coûteux et produira probablement unOutOfMemoryException.

Heureusement, lors de l'utilisation des coroutines, ce n'est pas un cas. Nous pouvons exécuter autant d'opérations de blocage que nous voulons. Sous le capot, ces opérations seront gérées par un nombre fixe de threads sans création excessive de threads:

@Test
fun givenHugeAmountOfCoroutines_whenStartIt_thenShouldExecuteItWithoutOutOfMemory() {
    runBlocking {
        // given
        val counter = AtomicInteger(0)
        val numberOfCoroutines = 100_000

        // when
        val jobs = List(numberOfCoroutines) {
            launch(CommonPool) {
                delay(1000L)
                counter.incrementAndGet()
            }
        }
        jobs.forEach { it.join() }

        // then
        assertEquals(counter.get(), numberOfCoroutines)
    }
}

Notez que nous exécutons 100 000 coroutines et que chaque exécution ajoute un retard substantiel. Néanmoins, il n'est pas nécessaire de créer trop de threads car ces opérations sont exécutées de manière asynchrone en utilisant le thread desCommonPool.

6. Annulation et délais d'expiration

Parfois, après avoir déclenché un calcul asynchrone de longue durée, nous souhaitons l'annuler car nous ne sommes plus intéressés par le résultat.

Lorsque nous démarrons notre action asynchrone avec la coroutinelaunch(), nous pouvons examiner l'indicateurisActive. Cet indicateur est mis à false chaque fois que le thread principal invoque la méthodecancel() sur l'instance desJob:

@Test
fun givenCancellableJob_whenRequestForCancel_thenShouldQuit() {
    runBlocking {
        // given
        val job = launch(CommonPool) {
            while (isActive) {
                println("is working")
            }
        }

        delay(1300L)

        // when
        job.cancel()

        // then cancel successfully

    }
}

C'est un très élégant eteasy way to use the cancellation mechanism. Dans l'action asynchrone, il suffit de vérifier si l'indicateurisActive est égal àfalse et d'annuler notre traitement.

Lorsque nous demandons un traitement et que nous ne savons pas combien de temps ce calcul prendra, il est conseillé de définir le délai d’expiration pour une telle action. Si le traitement ne se termine pas dans le délai imparti, nous obtiendrons une exception et nous pourrons y réagir de manière appropriée.

Par exemple, nous pouvons réessayer l'action:

@Test(expected = CancellationException::class)
fun givenAsyncAction_whenDeclareTimeout_thenShouldFinishWhenTimedOut() {
    runBlocking {
        withTimeout(1300L) {
            repeat(1000) { i ->
                println("Some expensive computation $i ...")
                delay(500L)
            }
        }
    }
}

Si nous ne définissons pas de délai d’expiration, il est possible que notre thread soit bloqué pour toujours car ce calcul se bloquera. Nous ne pouvons pas gérer ce cas dans notre code si le délai d'attente n'est pas défini.

7. Exécution simultanée d'actions asynchrones

Disons que nous devons lancer deux actions asynchrones simultanément et attendre leurs résultats par la suite. Si notre traitement prend une seconde et que nous devons l'exécuter deux fois, l'exécution du blocage synchrone sera de deux secondes.

Il serait préférable de pouvoir exécuter ces deux actions dans des threads séparés et d'attendre ces résultats dans le thread principal.

We can leverage the async() coroutine to achieve this en démarrant le traitement dans deux threads séparés simultanément:

@Test
fun givenHaveTwoExpensiveAction_whenExecuteThemAsync_thenTheyShouldRunConcurrently() {
    runBlocking {
        val delay = 1000L
        val time = measureTimeMillis {
            // given
            val one = async(CommonPool) {
                someExpensiveComputation(delay)
            }
            val two = async(CommonPool) {
                someExpensiveComputation(delay)
            }

            // when
            runBlocking {
                one.await()
                two.await()
            }
        }

        // then
        assertTrue(time < delay * 2)
    }
}

Après avoir soumis les deux calculs coûteux, nous suspendons la coroutine en exécutant l'appelrunBlocking(). Une fois que les résultatsone ettwo sont disponibles, la coroutine reprendra et les résultats seront renvoyés. L'exécution de deux tâches de cette manière devrait prendre environ une seconde.

Nous pouvons passerCoroutineStart.LAZY comme deuxième argument à la méthodeasync(), mais cela signifiera que le calcul asynchrone ne sera pas lancé tant qu'il ne sera pas demandé. Parce que nous demandons un calcul dans la coroutine derunBlocking, cela signifie que l'appel àtwo.await() ne sera effectué qu'une fois que leone.await() sera terminé:

@Test
fun givenTwoExpensiveAction_whenExecuteThemLazy_thenTheyShouldNotConcurrently() {
    runBlocking {
        val delay = 1000L
        val time = measureTimeMillis {
            // given
            val one
              = async(CommonPool, CoroutineStart.LAZY) {
                someExpensiveComputation(delay)
              }
            val two
              = async(CommonPool, CoroutineStart.LAZY) {
                someExpensiveComputation(delay)
            }

            // when
            runBlocking {
                one.await()
                two.await()
            }
        }

        // then
        assertTrue(time > delay * 2)
    }
}

The laziness of the execution in this particular example causes our code to run synchronously. Cela se produit parce que lorsque nous appelonsawait(), le thread principal est bloqué et seulement après que la tâcheone ait terminé, la tâchetwo sera déclenchée.

Nous devons être conscients d'effectuer des actions asynchrones de manière lâche car elles peuvent s'exécuter de manière bloquante.

8. Conclusion

Dans cet article, nous avons examiné les bases des coroutines de Kotlin.

Nous avons vu quebuildSequence est le bloc de construction principal de chaque coroutine. Nous avons décrit l’apparence du flux d’exécution dans ce style de programmation Continuation-passante.

Enfin, nous avons examiné la bibliothèquekotlinx-coroutines qui contient de nombreuses constructions très utiles pour créer des programmes asynchrones.

L'implémentation de tous ces exemples et extraits de code peut être trouvée dans leGitHub project - il s'agit d'un projet Maven, il devrait donc être facile à importer et à exécuter tel quel.