Kotlin coroutines garantit «avant-avant»?

10

Les coroutines Kotlin offrent-elles des garanties de «survenance avant»?

Par exemple, existe-t-il une garantie «passe avant» entre l'écriture mutableVaret la lecture ultérieure sur (potentiellement) un autre thread dans ce cas:

suspend fun doSomething() {
    var mutableVar = 0
    withContext(Dispatchers.IO) {
        mutableVar = 1
    }
    System.out.println("value: $mutableVar")
}

Éditer:

Un exemple supplémentaire clarifiera peut-être mieux la question, car il s'agit davantage de Kotlin-ish (sauf pour la mutabilité). Ce code est-il sûr pour les threads:

suspend fun doSomething() {
    var data = withContext(Dispatchers.IO) {
        Data(1)
    }
    System.out.println("value: ${data.data}")
}

private data class Data(var data: Int)
Vasiliy
la source
Notez que lors de l'exécution sur la JVM, Kotlin utilise le même modèle de mémoire que Java.
Slaw
1
@Slaw, je le sais. Cependant, il y a beaucoup de magie sous le capot. Par conséquent, j'aimerais comprendre s'il y a des garanties avant-que je reçois des coroutines, ou tout est sur moi.
Vasiliy
Si quelque chose, votre deuxième exemple présente un scénario encore plus simple: il utilise simplement un objet créé à l'intérieur withContext, tandis que le 1er exemple le crée d'abord, mute à l'intérieur withContext, puis lit après withContext. Ainsi, le 1er exemple exerce plus de fonctionnalités de sécurité des threads.
Marko Topolnik
... et les deux exemples n'exercent que l'aspect "ordre du programme" de ce qui se passe avant, le plus trivial. Je parle au niveau des coroutines ici, pas de la JVM sous-jacente. Donc, fondamentalement, vous demandez si les coroutines Kotlin sont si gravement cassées qu'elles ne fournissent même pas l'ordre du programme avant.
Marko Topolnik
1
@MarkoTopolnik, corrigez-moi si je me trompe, mais JLS ne garantit que "l'ordre du programme se produit avant" pour une exécution sur le même thread. Maintenant, avec les coroutines, même si le code semble séquentiel, dans la pratique, il existe des machines qui le déchargent sur différents threads. Je comprends votre point "c'est une telle garantie de base que je ne perdrais même pas mon temps à le vérifier" (à partir d'un autre commentaire), mais j'ai posé cette question pour obtenir une réponse rigoureuse. Je suis presque sûr que les exemples que j'ai écrits sont thread-safe, mais je veux comprendre pourquoi.
Vasiliy

Réponses:

6

Le code que vous avez écrit a trois accès à l'état partagé:

var mutableVar = 0                        // access 1, init
withContext(Dispatchers.IO) {
    mutableVar = 1                        // access 2, write
}
System.out.println("value: $mutableVar")  // access 3, read

Les trois accès sont strictement séquentiellement ordonnés, sans concurrence entre eux, et vous pouvez être assuré que l'infrastructure de Kotlin prend en charge l'établissement d'un bord avant lors du transfert vers le IOpool de threads et le retour à votre coroutine appelante.

Voici un exemple équivalent qui peut peut-être sembler plus convaincant:

launch(Dispatchers.Default) {
    var mutableVar = 0             // 1
    delay(1)
    mutableVar = 1                 // 2
    delay(1)
    println("value: $mutableVar")  // 3
}

Étant donné que delayc'est une fonction suspendue et que nous utilisons le Defaultrépartiteur soutenu par un pool de threads, les lignes 1, 2 et 3 peuvent chacune s'exécuter sur un thread différent. Par conséquent, votre question sur les garanties avant- propos s'applique également à cet exemple. En revanche, dans ce cas il est (j'espère) tout à fait évident que le comportement de ce code est conforme aux principes d'exécution séquentielle.

Marko Topolnik
la source
1
Merci. C'est en fait la partie après "rassurez-vous" qui m'a motivé à poser cette question. Y a-t-il des liens vers des documents que je pourrais lire? Alternativement, les liens vers le code source où cela se produit avant que le bord ne soit établi seraient également d'une grande aide (jointure, synchronisation ou toute autre méthode).
Vasiliy
1
C'est une telle garantie de base que je ne perdrais même pas mon temps à le vérifier. Sous le capot, cela se résume à executorService.submit()et il existe un mécanisme typique d'attente à la fin de la tâche (terminer une tâche CompletableFutureou quelque chose de similaire). Du point de vue des coroutines Kotlin, il n'y a pas du tout de concurrence ici.
Marko Topolnik
1
Vous pouvez penser que votre question est analogue à la question «le système d'exploitation garantit-il que cela se produit avant de suspendre un thread puis de le reprendre sur un autre noyau? Les threads sont pour coroutiner ce que sont les cœurs CPU pour les threads.
Marko Topolnik
1
Merci pour votre explication. Cependant, j'ai posé cette question pour comprendre pourquoi cela fonctionne. Je vois votre point, mais, jusqu'à présent, ce n'est pas la réponse rigoureuse que je recherche.
Vasiliy
2
Eh bien ... en fait, je ne pense pas que ce fil a établi que le code est séquentiel. Il l'a sûrement affirmé. Moi aussi, je serais intéressé à voir le mécanisme qui garantit que l'exemple se comporte comme prévu, sans affecter les performances.
G. Blake Meike
3

Les coroutines de Kotlin prévoient des garanties avant les garanties.

La règle est la suivante: à l'intérieur d'une coroutine, le code avant un appel de fonction de suspension se produit avant le code après l'appel de suspension.

Vous devriez penser aux coroutines comme s'il s'agissait de threads réguliers:

Même si une coroutine dans Kotlin peut s'exécuter sur plusieurs threads, c'est comme un thread d'un point de vue d'état mutable. Deux actions dans la même coroutine ne peuvent pas être simultanées.

Source: https://proandroiddev.com/what-is-concurrent-access-to-mutable-state-f386e5cb8292

Revenons à l'exemple de code. Capturer des variables dans des corps de fonctions lambda n'est pas la meilleure idée, surtout lorsque lambda est une coroutine. Le code avant un lambda ne se produit pas avant le code à l'intérieur.

Voir https://youtrack.jetbrains.com/issue/KT-15514

Sergei Voitovich
la source
En fait, la règle est la suivante: le code avant un appel de fonction de suspension se produit - avant que le code à l'intérieur de la fonction de suspension ne se produise - avant le code après l'appel de suspension. Ceci, à son tour, peut être généralisé à "l'ordre du programme du code est également l'ordre du passe-avant du code ". Notez l'absence de quelque chose de spécifique aux fonctions suspendables dans cette déclaration.
Marko Topolnik