it-swarm.com.de

Wie man auf mehrere Futures wartet

Angenommen, ich habe mehrere Zukünfte und muss warten, bis entweder einer von ihnen fehlschlägt oder alle von ihnen erfolgreich sind.

Zum Beispiel: Es gibt 3 Futures: f1, f2, f3.

  • Ob f1 erfolgreich und f2 scheitert ich warte nicht auf f3 (und zurück Fehler an den Client).

  • Ob f2 schlägt fehl, während f1 und f3 noch laufen Ich warte nicht auf sie (und zurück Fehler)

  • Ob f1 erfolgreich und dann f2 erfolgreich Ich warte weiter auf f3.

Wie würden Sie es umsetzen?

82
Michael

Sie könnten stattdessen eine For-Comprehension wie folgt verwenden:

val fut1 = Future{...}
val fut2 = Future{...}
val fut3 = Future{...}

val aggFut = for{
  f1Result <- fut1
  f2Result <- fut2
  f3Result <- fut3
} yield (f1Result, f2Result, f3Result)

In diesem Beispiel werden die Futures 1, 2 und 3 parallel gestartet. Dann warten wir zum Verständnis, bis die Ergebnisse 1 und dann 2 und dann 3 verfügbar sind. Wenn entweder 1 oder 2 ausfallen, werden wir nicht mehr auf 3 warten. Wenn alle 3 erfolgreich sind, enthält der Wert aggFut ein Tupel mit 3 Slots, entsprechend den Ergebnissen der 3 Futures.

Wenn Sie nun das Verhalten benötigen, bei dem Sie aufhören möchten zu warten, wenn fut2 zuerst ausfällt, werden die Dinge etwas schwieriger. Im obigen Beispiel müssten Sie warten, bis fut1 abgeschlossen ist, bevor Sie feststellen, dass fut2 fehlgeschlagen ist. Um dies zu lösen, können Sie Folgendes versuchen:

  val fut1 = Future{Thread.sleep(3000);1}
  val fut2 = Promise.failed(new RuntimeException("boo")).future
  val fut3 = Future{Thread.sleep(1000);3}

  def processFutures(futures:Map[Int,Future[Int]], values:List[Any], prom:Promise[List[Any]]):Future[List[Any]] = {
    val fut = if (futures.size == 1) futures.head._2
    else Future.firstCompletedOf(futures.values)

    fut onComplete{
      case Success(value) if (futures.size == 1)=> 
        prom.success(value :: values)

      case Success(value) =>
        processFutures(futures - value, value :: values, prom)

      case Failure(ex) => prom.failure(ex)
    }
    prom.future
  }

  val aggFut = processFutures(Map(1 -> fut1, 2 -> fut2, 3 -> fut3), List(), Promise[List[Any]]())
  aggFut onComplete{
    case value => println(value)
  }

Das funktioniert jetzt korrekt, aber das Problem besteht darin, zu wissen, welches Future aus dem Map zu entfernen ist, wenn eines erfolgreich abgeschlossen wurde. Solange Sie eine Möglichkeit haben, ein Ergebnis richtig mit der Zukunft zu korrelieren, die dieses Ergebnis hervorgebracht hat, funktioniert so etwas. Es entfernt einfach rekursiv die fertigen Futures von der Karte und ruft dann Future.firstCompletedOf Für die verbleibenden Futures auf, bis keine mehr übrig sind, um die Ergebnisse auf dem Weg zu sammeln. Es ist nicht schön, aber wenn Sie das Verhalten, von dem Sie sprechen, wirklich brauchen, könnte dies oder etwas Ähnliches funktionieren.

78
cmbaxter

Sie können ein Versprechen verwenden und ihm entweder den ersten Fehler oder den endgültigen abgeschlossenen Gesamterfolg senden:

def sequenceOrBailOut[A, M[_] <: TraversableOnce[_]](in: M[Future[A]] with TraversableOnce[Future[A]])(implicit cbf: CanBuildFrom[M[Future[A]], A, M[A]], executor: ExecutionContext): Future[M[A]] = {
  val p = Promise[M[A]]()

  // the first Future to fail completes the promise
  in.foreach(_.onFailure{case i => p.tryFailure(i)})

  // if the whole sequence succeeds (i.e. no failures)
  // then the promise is completed with the aggregated success
  Future.sequence(in).foreach(p trySuccess _)

  p.future
}

Dann können Sie Await auf das resultierende Future anwenden, wenn Sie es blockieren möchten, oder einfach map in etwas anderes.

Der Unterschied zum Verständnis besteht darin, dass hier der Fehler des ersten Fehlers auftritt, während beim Verständnis der erste Fehler in der Durchlaufreihenfolge der Eingabeauflistung auftritt (auch wenn ein anderer zuerst fehlgeschlagen ist). Beispielsweise:

val f1 = Future { Thread.sleep(1000) ; 5 / 0 }
val f2 = Future { 5 }
val f3 = Future { None.get }

Future.sequence(List(f1,f2,f3)).onFailure{case i => println(i)}
// this waits one second, then prints "Java.lang.ArithmeticException: / by zero"
// the first to fail in traversal order

Und:

val f1 = Future { Thread.sleep(1000) ; 5 / 0 }
val f2 = Future { 5 }
val f3 = Future { None.get }

sequenceOrBailOut(List(f1,f2,f3)).onFailure{case i => println(i)}
// this immediately prints "Java.util.NoSuchElementException: None.get"
// the 'actual' first to fail (usually...)
// and it returns early (it does not wait 1 sec)
32
gourlaysama

Hier ist eine Lösung ohne Schauspieler.

import scala.util._
import scala.concurrent._
import Java.util.concurrent.atomic.AtomicInteger

// Nondeterministic.
// If any failure, return it immediately, else return the final success.
def allSucceed[T](fs: Future[T]*): Future[T] = {
  val remaining = new AtomicInteger(fs.length)

  val p = promise[T]

  fs foreach {
    _ onComplete {
      case s @ Success(_) => {
        if (remaining.decrementAndGet() == 0) {
          // Arbitrarily return the final success
          p tryComplete s
        }
      }
      case f @ Failure(_) => {
        p tryComplete f
      }
    }
  }

  p.future
}
7
FranklinChen

Zu diesem Zweck würde ich einen Akka-Schauspieler verwenden. Im Gegensatz zum For-Comprehension schlägt es fehl, sobald eine der Futures ausfällt. In diesem Sinne ist es also ein bisschen effizienter.

class ResultCombiner(futs: Future[_]*) extends Actor {

  var origSender: ActorRef = null
  var futsRemaining: Set[Future[_]] = futs.toSet

  override def receive = {
    case () =>
      origSender = sender
      for(f <- futs)
        f.onComplete(result => self ! if(result.isSuccess) f else false)
    case false =>
      origSender ! SomethingFailed
    case f: Future[_] =>
      futsRemaining -= f
      if(futsRemaining.isEmpty) origSender ! EverythingSucceeded
  }

}

sealed trait Result
case object SomethingFailed extends Result
case object EverythingSucceeded extends Result

Erstellen Sie dann den Akteur, senden Sie ihm eine Nachricht (damit er weiß, wohin er die Antwort senden soll) und warten Sie auf eine Antwort.

val actor = actorSystem.actorOf(Props(new ResultCombiner(f1, f2, f3)))
try {
  val f4: Future[Result] = actor ? ()
  implicit val timeout = new Timeout(30 seconds) // or whatever
  Await.result(f4, timeout.duration).asInstanceOf[Result] match {
    case SomethingFailed => println("Oh noes!")
    case EverythingSucceeded => println("It all worked!")
  }
} finally {
  // Avoid memory leaks: destroy the actor
  actor ! PoisonPill
}
5
Robin Green

Sie können dies allein mit Futures tun. Hier ist eine Implementierung. Beachten Sie, dass die Ausführung nicht vorzeitig beendet wird! In diesem Fall müssen Sie etwas Raffinierteres tun (und die Unterbrechung wahrscheinlich selbst implementieren). Wenn Sie jedoch nicht auf etwas warten möchten, das nicht funktioniert, müssen Sie auf das Ende des ersten Vorgangs warten und anhalten, wenn entweder nichts mehr übrig ist oder Sie eine Ausnahmebedingung treffen:

import scala.annotation.tailrec
import scala.util.{Try, Success, Failure}
import scala.concurrent._
import scala.concurrent.duration.Duration
import ExecutionContext.Implicits.global

@tailrec def awaitSuccess[A](fs: Seq[Future[A]], done: Seq[A] = Seq()): 
Either[Throwable, Seq[A]] = {
  val first = Future.firstCompletedOf(fs)
  Await.ready(first, Duration.Inf).value match {
    case None => awaitSuccess(fs, done)  // Shouldn't happen!
    case Some(Failure(e)) => Left(e)
    case Some(Success(_)) =>
      val (complete, running) = fs.partition(_.isCompleted)
      val answers = complete.flatMap(_.value)
      answers.find(_.isFailure) match {
        case Some(Failure(e)) => Left(e)
        case _ =>
          if (running.length > 0) awaitSuccess(running, answers.map(_.get) ++: done)
          else Right( answers.map(_.get) ++: done )
      }
  }
}

Hier ist ein Beispiel in Aktion, wenn alles in Ordnung ist:

scala> awaitSuccess(Seq(Future{ println("Hi!") }, 
  Future{ Thread.sleep(1000); println("Fancy meeting you here!") },
  Future{ Thread.sleep(2000); println("Bye!") }
))
Hi!
Fancy meeting you here!
Bye!
res1: Either[Throwable,Seq[Unit]] = Right(List((), (), ()))

Aber wenn etwas schief geht:

scala> awaitSuccess(Seq(Future{ println("Hi!") }, 
  Future{ Thread.sleep(1000); throw new Exception("boo"); () }, 
  Future{ Thread.sleep(2000); println("Bye!") }
))
Hi!
res2: Either[Throwable,Seq[Unit]] = Left(Java.lang.Exception: boo)

scala> Bye!
5
Rex Kerr

Diese Frage wurde beantwortet, aber ich poste meine Wertklassenlösung (Wertklassen wurden in 2.10 hinzugefügt), da es hier keine gibt. Bitte zögern Sie nicht zu kritisieren.

  implicit class Sugar_PimpMyFuture[T](val self: Future[T]) extends AnyVal {
    def concurrently = ConcurrentFuture(self)
  }
  case class ConcurrentFuture[A](future: Future[A]) extends AnyVal {
    def map[B](f: Future[A] => Future[B]) : ConcurrentFuture[B] = ConcurrentFuture(f(future))
    def flatMap[B](f: Future[A] => ConcurrentFuture[B]) : ConcurrentFuture[B] = concurrentFutureFlatMap(this, f) // work around no nested class in value class
  }
  def concurrentFutureFlatMap[A,B](outer: ConcurrentFuture[A], f: Future[A] => ConcurrentFuture[B]) : ConcurrentFuture[B] = {
    val p = Promise[B]()
    val inner = f(outer.future)
    inner.future onFailure { case t => p.tryFailure(t) }
    outer.future onFailure { case t => p.tryFailure(t) }
    inner.future onSuccess { case b => p.trySuccess(b) }
    ConcurrentFuture(p.future)
  }

ConcurrentFuture ist ein No-Overhead-Future-Wrapper, der die Standardzukunft map/flatMap von do-this-then-that in all-and-fail-if-any-fail ändert. Verwendung:

def func1 : Future[Int] = Future { println("f1!");throw new RuntimeException; 1 }
def func2 : Future[String] = Future { Thread.sleep(2000);println("f2!");"f2" }
def func3 : Future[Double] = Future { Thread.sleep(2000);println("f3!");42.0 }

val f : Future[(Int,String,Double)] = {
  for {
    f1 <- func1.concurrently
    f2 <- func2.concurrently
    f3 <- func3.concurrently
  } yield for {
   v1 <- f1
   v2 <- f2
   v3 <- f3
  } yield (v1,v2,v3)
}.future
f.onFailure { case t => println("future failed $t") }

Im obigen Beispiel werden f1, f2 und f3 gleichzeitig ausgeführt. Wenn ein Fehler in einer beliebigen Reihenfolge auftritt, schlägt die Zukunft des Tupels sofort fehl.

4
lancegatlin

Vielleicht möchten Sie die zukünftige API von Twitter überprüfen. Insbesondere die Future.collect-Methode. Es macht genau das, was Sie wollen: https://Twitter.github.io/scala_school/finagle.html

Der Quellcode Future.scala ist hier verfügbar: https://github.com/Twitter/util/blob/master/util-core/src/main/scala/com/Twitter/util/Future.scala

4
JBakouny

Sie können dies verwenden:

val l = List(1, 6, 8)

val f = l.map{
  i => future {
    println("future " +i)
    Thread.sleep(i* 1000)
    if (i == 12)
      throw new Exception("6 is not legal.")
    i
  }
}

val f1 = Future.sequence(f)

f1 onSuccess{
  case l => {
    logInfo("onSuccess")
    l.foreach(i => {

      logInfo("h : " + i)

    })
  }
}

f1 onFailure{
  case l => {
    logInfo("onFailure")
  }
2
igreenfield