web-dev-qa-db-fra.com

Scala async/wait et parallélisation

J'apprends les utilisations de async/wait dans Scala. J'ai lu ceci dans https://github.com/scala/async

Théoriquement, ce code est asynchrone (non bloquant), mais il n'est pas parallélisé:

def slowCalcFuture: Future[Int] = ...             
def combined: Future[Int] = async {               
   await(slowCalcFuture) + await(slowCalcFuture)
}
val x: Int = Await.result(combined, 10.seconds)    

alors que cet autre est parallélisé:

def combined: Future[Int] = async {
  val future1 = slowCalcFuture
  val future2 = slowCalcFuture
  await(future1) + await(future2)
}

La seule différence entre eux est l'utilisation de variables intermédiaires ..__ Comment cela peut-il affecter la parallélisation?

35
Sanete

Comme il ressemble à async & await en C #, je peux peut-être vous donner un aperçu. En C #, il est de règle générale que Task qui peut être attendu devrait être retourné "à chaud", c'est-à-dire déjà en cours d'exécution. Je suppose que c'est la même chose en Scala, où la Future renvoyée par la fonction ne doit pas nécessairement être lancée explicitement, elle est simplement «en cours d'exécution» après avoir été appelée. Si c'est pas le cas, alors ce qui suit est pure (et probablement pas vraie) spéculation.

Analysons le premier cas:

async {
    await(slowCalcFuture) + await(slowCalcFuture)
}

Nous arrivons à ce bloc et frappons la première attente:

async {
    await(slowCalcFuture) + await(slowCalcFuture)
    ^^^^^
}

Ok, nous attendons donc de manière asynchrone la fin du calcul. Quand c'est fini, nous continuons avec l'analyse du bloc:

async {
    await(slowCalcFuture) + await(slowCalcFuture)
                            ^^^^^
}

Deuxième attente, nous attendons donc de manière asynchrone la fin du deuxième calcul. Ensuite, nous pouvons calculer le résultat final en ajoutant deux entiers.

Comme vous pouvez le constater, nous avançons pas à pas, dans l’attente des Futures au fur et à mesure de leur arrivée.

Jetons un coup d'oeil au deuxième exemple:

async {
  val future1 = slowCalcFuture
  val future2 = slowCalcFuture
  await(future1) + await(future2)
}

OK, alors voici ce qui se passe (probablement):

async {
  val future1 = slowCalcFuture // >> first future is started, but not awaited
  val future2 = slowCalcFuture // >> second future is started, but not awaited
  await(future1) + await(future2)
  ^^^^^
}

Ensuite, nous attendons la première Future, mais les deux futurs sont en cours d’exécution. Lorsque le premier retourne, le second est peut-être déjà terminé (nous aurons le résultat immédiatement disponible) ou nous devrons peut-être attendre un peu plus longtemps.

Maintenant, il est clair que le deuxième exemple exécute deux calculs en parallèle, puis attend que les deux soient terminés. Quand les deux sont prêts, ça revient. Le premier exemple exécute les calculs de manière non bloquante, mais séquentielle.

47
Patryk Ćwiek

la réponse de Patryk est correcte si un peu difficile à suivre. la chose principale à comprendre à propos de async/wait est que c’est juste une autre façon de faire Future's flatMap. il n'y a pas de magie de concurrence dans les coulisses. tous les appels à l'intérieur d'un bloc asynchrone sont séquentiels, y compris wait qui ne bloque pas le thread en cours d'exécution mais enveloppe plutôt le reste du bloc async dans une fermeture et le transmet sous la forme d'un callback à la fin de la Future nous attendons. Ainsi, dans le premier morceau de code, le deuxième calcul ne commence pas tant que la première attente n'a pas été terminée car personne ne l'avait démarré auparavant.

22
Mikha

Dans le premier cas, vous créez un nouveau thread pour exécuter un avenir lent et l'attendre lors d'un seul appel. Ainsi, l'invocation du deuxième avenir lent est effectuée une fois que le premier est terminé.

Dans le second cas, lorsque val future1 = slowCalcFuture est appelé, il crée effectivement un nouveau thread, passe le pointeur sur la fonction "slowCalcFuture" au thread et dit "exécutez-le s'il vous plaît". Il faut autant de temps qu'il est nécessaire pour extraire une instance de thread du pool de thread et passer un pointeur sur une fonction pour l'instance de thread. Ce qui peut être considéré comme instantané. Donc, étant donné que val future1 = slowCalcFuture est traduit en opérations "get thread and pass pointer", il est terminé en un rien de temps et la ligne suivante est exécutée sans délai val future2 = slowCalcFuture. Feauture 2 doit également être exécuté sans délai.

La différence fondamentale entre val future1 = slowCalcFuture et await(slowCalcFuture) est la même qu'entre demander à quelqu'un de vous préparer du café et attendre que votre café soit prêt. Demander prend 2 secondes: il faut dire: "pouvez-vous me préparer du café s'il vous plaît?". Mais attendre que le café soit prêt prendra 4 minutes.

Une éventuelle modification de cette tâche pourrait être en attente de la première réponse disponible. Par exemple, vous souhaitez vous connecter à n’importe quel serveur d’un cluster. Vous émettez des demandes de connexion à chaque serveur que vous connaissez et le premier qui répond sera votre serveur. Vous pouvez le faire avec: Future.firstCompletedOf(Array(slowCalcFuture, slowCalcFuture))

0
Vadim Chekan