web-dev-qa-db-fra.com

Scala en attente d'une séquence de futurs

J'espérais que le code comme suit attendrait les deux futurs, mais ce n'est pas le cas.

object Fiddle {
  val f1 = Future {
    throw new Throwable("baaa") // emulating a future that bumped into an exception
  }

  val f2 = Future {
    Thread.sleep(3000L) // emulating a future that takes a bit longer to complete
    2
  }

  val lf = List(f1, f2) // in the general case, this would be a dynamically sized list

  val seq = Future.sequence(lf) 

  seq.onComplete {
    _ => lf.foreach(f => println(f.isCompleted))
  }
}

val a = FuturesSequence

Je supposais seq.onComplete attendrait qu'ils se terminent tous avant de s'achever, mais pas ainsi; il en résulte:

true
false

.sequence était un peu difficile à suivre dans la source de scala.concurrent.Future, je me demande comment j'implémenterais un parallèle qui attend tous les futurs originaux d'une séquence (de taille dynamique), ou quel pourrait être le problème ici.

Modifier: Une question connexe: https://worldbuilding.stackexchange.com/questions/12348/how-do-you-prove- vous êtes du futur :)

26
matanster

Une approche courante pour attendre tous les résultats (échoués ou non) consiste à "élever" les échecs dans une nouvelle représentation à l'intérieur du futur, de sorte que tous les futurs se terminent avec un certain résultat (bien qu'ils puissent se terminer avec un résultat qui représente un échec). Une façon naturelle d'obtenir cela est de passer à un Try.

implémentation de futures de Twitter fournit une méthode liftToTry qui rend cela trivial, mais vous pouvez faire quelque chose de similaire avec l'implémentation de la bibliothèque standard:

import scala.util.{ Failure, Success, Try }

val lifted: List[Future[Try[Int]]] = List(f1, f2).map(
  _.map(Success(_)).recover { case t => Failure(t) }
)

Maintenant, Future.sequence(lifted) sera terminée lorsque tous les futurs seront terminés, et représentera les succès et les échecs en utilisant Try.

Et donc, une solution générique pour attendre tous les futurs originaux d'une séquence de futurs peut ressembler à ceci, en supposant qu'un contexte d'exécution est bien sûr implicitement disponible.

  import scala.util.{ Failure, Success, Try }

  private def lift[T](futures: Seq[Future[T]]) = 
    futures.map(_.map { Success(_) }.recover { case t => Failure(t) })

  def waitAll[T](futures: Seq[Future[T]]) =
    Future.sequence(lift(futures)) // having neutralized exception completions through the lifting, .sequence can now be used

  waitAll(SeqOfFutures).map { 
    // do whatever with the completed futures
  }
37
Travis Brown

Un Future produit par Future.sequence se termine lorsque:

  • tous les contrats à terme se sont terminés avec succès, ou
  • l'un des futurs a échoué

Le deuxième point est ce qui se passe dans votre cas, et il est logique de terminer dès que l'un des Future encapsulés a échoué, car l'habillage Future ne peut contenir qu'un seul Throwable dans le cas d'échec. Inutile d'attendre les autres futurs car le résultat sera le même échec.

21
Ionuț G. Stan

Ceci est un exemple qui prend en charge la réponse précédente. Il existe un moyen simple de le faire en utilisant uniquement les API standard Scala.

Dans l'exemple, je crée 3 futures. Celles-ci se termineront respectivement à 5, 7 et 9 secondes. L'appel à Await.result Se bloquera jusqu'à ce que tous les contrats à terme soient résolus. Une fois les 3 futures terminés, a sera réglé sur List(5,7,9) et l'exécution continuera.

De plus, si une exception est levée dans l'un des futurs, Await.result Débloquera immédiatement et lèvera l'exception. Décommentez la ligne Exception(...) pour voir cela en action.

  try {
    val a = Await.result(Future.sequence(Seq(
      Future({
        blocking {
          Thread.sleep(5000)
        }
        System.err.println("A")
        5
      }),
      Future({
        blocking {
          Thread.sleep(7000)
        }
        System.err.println("B")
        7
        //throw new Exception("Ha!")
      }),
      Future({
        blocking {
          Thread.sleep(9000)
        }
        System.err.println("C")
        9
      }))),
      Duration("100 sec"))

    System.err.println(a)
  } catch {
    case e: Exception ⇒
      e.printStackTrace()
  }
3
Jason Smith

Nous pouvons enrichir Seq[Future[T]] avec sa propre méthode onComplete via une classe implicite:

  def lift[T](f: Future[T])(implicit ec: ExecutionContext): Future[Try[T]] =
    f map { Success(_) } recover { case e => Failure(e) }

  def lift[T](fs: Seq[Future[T]])(implicit ec: ExecutionContext): Seq[Future[Try[T]]] =
    fs map { lift(_) }

  implicit class RichSeqFuture[+T](val fs: Seq[Future[T]]) extends AnyVal {
    def onComplete[U](f: Seq[Try[T]] => U)(implicit ec: ExecutionContext) = {
      Future.sequence(lift(fs)) onComplete {
        case Success(s) => f(s)
        case Failure(e) => throw e // will never happen, because of the Try lifting
      }
    }
  }

Ensuite, dans votre MWE particulier, vous pouvez faire:

  val f1 = Future {
    throw new Throwable("baaa") // emulating a future that bumped into an exception
  }

  val f2 = Future {
    Thread.sleep(3000L) // emulating a future that takes a bit longer to complete
    2
  }

  val lf = List(f1, f2)

  lf onComplete { _ map {
    case Success(v) => ???
    case Failure(e) => ???
  }}

Cette solution a l'avantage de vous permettre d'appeler un onComplete sur une séquence de futures comme vous le feriez sur un seul futur.

1
Bruno