web-dev-qa-db-fra.com

Asynchrone IO in Scala with futures

Disons que je reçois une (potentiellement grande) liste d'images à télécharger à partir de certaines URL. J'utilise Scala, donc ce que je ferais c'est:

import scala.actors.Futures._

// Retrieve URLs from somewhere
val urls: List[String] = ...

// Download image (blocking operation)
val fimages: List[Future[...]] = urls.map (url => future { download url })

// Do something (display) when complete
fimages.foreach (_.foreach (display _))

Je suis un peu nouveau pour Scala, donc cela me semble toujours un peu magique:

  • Est-ce que c'est la bonne façon de le faire? Des alternatives si ce n'est pas le cas?
  • Si j'ai 100 images à télécharger, cela créera-t-il 100 threads à la fois ou utilisera-t-il un pool de threads?
  • La dernière instruction (display _) être exécuté sur le thread principal, et sinon, comment puis-je m'assurer qu'il l'est?

Merci pour vos conseils!

67
F.X.

Utilisez Futures dans Scala 2.10. Ils étaient un travail conjoint entre l'équipe Scala, l'équipe Akka et Twitter pour atteindre une future API et une implémentation plus standardisées à utiliser) Nous venons de publier un guide sur: http://docs.scala-lang.org/overviews/core/futures.html

Au-delà d'être complètement non bloquant (par défaut, bien que nous fournissions la possibilité de faire des opérations de blocage gérées) et composable, les futurs 2.10 de Scala sont livrés avec un pool de threads implicite pour exécuter vos tâches, ainsi que certains utilitaires pour gérer les délais d'attente.

import scala.concurrent.{future, blocking, Future, Await, ExecutionContext.Implicits.global}
import scala.concurrent.duration._

// Retrieve URLs from somewhere
val urls: List[String] = ...

// Download image (blocking operation)
val imagesFuts: List[Future[...]] = urls.map {
  url => future { blocking { download url } }
}

// Do something (display) when complete
val futImages: Future[List[...]] = Future.sequence(imagesFuts)
Await.result(futImages, 10 seconds).foreach(display)

Ci-dessus, nous importons d'abord un certain nombre de choses:

  • future: API pour créer un futur.
  • blocking: API pour le blocage géré.
  • Future: futur objet compagnon qui contient un certain nombre de méthodes utiles pour les collections de futurs.
  • Await: objet singleton utilisé pour bloquer sur un futur (transfert de son résultat au thread courant).
  • ExecutionContext.Implicits.global: le pool de threads global par défaut, un pool ForkJoin.
  • duration._: utilitaires de gestion des durées des temps morts.

imagesFuts reste en grande partie identique à ce que vous aviez fait à l'origine - la seule différence ici est que nous utilisons le blocage géré - blocking. Il informe le pool de threads que le bloc de code que vous lui transmettez contient des opérations de longue durée ou de blocage. Cela permet au pool de générer temporairement de nouveaux travailleurs pour s'assurer qu'il ne se produit jamais que tous les travailleurs sont bloqués. Cela est fait pour éviter la famine (verrouillage du pool de threads) dans les applications bloquantes. Notez que le pool de threads sait également lorsque le code dans un bloc de blocage géré est terminé - il supprimera donc le thread de travail de rechange à ce stade, ce qui signifie que le pool se rétrécira à sa taille attendue.

(Si vous voulez absolument empêcher la création de threads supplémentaires, vous devez utiliser une bibliothèque AsyncIO, telle que la bibliothèque NIO de Java.)

Ensuite, nous utilisons les méthodes de collecte de l'objet compagnon Future pour convertir imagesFuts à partir de List[Future[...]] à un Future[List[...]].

L'objet Await est la façon dont nous pouvons nous assurer que display est exécuté sur le thread appelant - Await.result force simplement le thread actuel à attendre que le futur qu'il soit passé soit terminé. (Cela utilise le blocage géré en interne.)

129
Heather Miller
val all = Future.traverse(urls){ url =>
  val f = future(download url) /*(downloadContext)*/
  f.onComplete(display)(displayContext)
  f
}
Await.result(all, ...)
  1. Utilisez scala.concurrent.Future en 2.10, qui est maintenant RC.
  2. qui utilise un ExecutionContext implicite
  3. Le nouveau document Future indique explicitement que onComplete (et foreach) peut évaluer immédiatement si la valeur est disponible. Les anciens acteurs de Future font la même chose. Selon ce dont vous avez besoin pour l'affichage, vous pouvez fournir un ExecutionContext approprié (par exemple, un exécuteur de thread unique). Si vous voulez simplement que le thread principal attende la fin du chargement, traverse vous offre un avenir à attendre.
5
som-snytt
  1. Oui, cela me semble bien, mais vous voudrez peut-être étudier des API plus puissantes Twitter-util ou Akka (Scala 2.10 aura une nouvelle bibliothèque Future dans ce style).

  2. Il utilise un pool de threads.

  3. Non, ce ne sera pas le cas. Pour cela, vous devez utiliser le mécanisme standard de votre boîte à outils GUI (SwingUtilities.invokeLater pour Swing ou Display.asyncExec pour SWT). Par exemple.

    fimages.foreach (_.foreach(im => SwingUtilities.invokeLater(new Runnable { display im })))
    
3
Alexey Romanov