Come aspettare diversi Futures?


86

Supponiamo che io sono parecchi futures e bisogno di aspettare fino a quando uno qualsiasi di loro non riesce o di tutti loro riescono.

Per esempio: Let ci sono 3 i futures: f1, f2, f3.

  • Se f1riesce e f2fallisce, non aspetto f3(e restituisco il fallimento al client).

  • Se f2fallisce mentre f1e f3sono ancora in esecuzione non li aspetto (e restituisco il fallimento )

  • Se f1riesce e poi ha f2successo continuo ad aspettare f3.

Come lo implementeresti?


un problema di Scala su questa domanda. issue.scala-lang.org/browse/SI-8994 l'API dovrebbe avere l'opzione per comportamenti diversi
WeiChing 林 煒 清

Risposte:


83

Potresti invece usare una per la comprensione come segue:

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

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

In questo esempio, i futures 1, 2 e 3 vengono avviati in parallelo. Quindi, nella fase di comprensione, aspettiamo che siano disponibili i risultati 1 e poi 2 e poi 3. Se 1 o 2 fallisce, non aspetteremo più 3. Se tutti e 3 hanno successo, aggFutval conterrà una tupla con 3 slot, corrispondenti ai risultati dei 3 futures.

Ora, se hai bisogno del comportamento in cui vuoi smettere di aspettare se diciamo fut2 fallisce prima, le cose si fanno un po 'più complicate. Nell'esempio sopra, dovresti aspettare il completamento di fut1 prima di realizzare che fut2 non è riuscito. Per risolverlo, potresti provare qualcosa del genere:

  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)
  }

Ora funziona correttamente, ma il problema deriva dal sapere quale Futurerimuovere da Mapquando uno è stato completato con successo. Finché hai un modo per correlare correttamente un risultato con il futuro che ha generato quel risultato, allora qualcosa di simile funziona. Continua semplicemente a rimuovere in modo ricorsivo i Futures completati dalla mappa e quindi a chiamare Future.firstCompletedOfi rimanenti Futuresfinché non ne rimangono più, raccogliendo i risultati lungo il percorso. Non è carino, ma se hai davvero bisogno del comportamento di cui parli, allora questo o qualcosa di simile potrebbe funzionare.


Grazie. Cosa succede se fut2fallisce prima fut1? Aspetteremo ancora fut1in quel caso? Se lo faremo non è esattamente quello che voglio.
Michael

Ma se 3 fallisce prima, aspettiamo ancora 1 e 2 quando potremmo tornare presto. Qualche modo per farlo senza richiedere la sequenza del futuro?
L'archetipo Paolo

È possibile installare un onFailuregestore per fut2fallire veloce e un onSuccesssu aggFutper il successo manico. Un successo su aggFutimplica è fut2stato completato con successo, quindi hai solo uno dei gestori chiamato.
pagoda_5b

Ho aggiunto un po 'di più alla mia risposta per mostrare una possibile soluzione per il fallimento rapido se uno dei futuri fallisce.
cmbaxter

1
Nel tuo primo esempio, 1 2 e 3 non vengono eseguiti in parallelo, quindi vengono eseguiti in serie. Provalo con le linee di stampa e guarda
bwawok

35

Puoi utilizzare una promessa e inviarle il primo errore o il successo aggregato completato finale:

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
}

Quindi puoi farlo Awaitsu quel risultato Futurese vuoi bloccare, o semplicemente mapfarlo in qualcos'altro.

La differenza con for Understanding è che qui ottieni l'errore del primo che fallisce, mentre con For Understanding ottieni il primo errore nell'ordine di attraversamento della raccolta di input (anche se un altro fallisce per primo). Per esempio:

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

E:

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)

7

Ecco una soluzione senza usare attori.

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
}

5

Puoi farlo solo con il futuro. Ecco un'implementazione. Nota che non terminerà presto l'esecuzione! In tal caso è necessario fare qualcosa di più sofisticato (e probabilmente implementare l'interruzione da soli). Ma se semplicemente non vuoi continuare ad aspettare qualcosa che non funzionerà, la chiave è continuare ad aspettare che la prima cosa finisca e fermarti quando non è rimasto nulla o colpisci un'eccezione:

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 )
      }
  }
}

Ecco un esempio in azione quando tutto funziona correttamente:

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((), (), ()))

Ma quando qualcosa va storto:

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!

1
Bella implementazione. Ma nota che se passi una sequenza vuota di futuri per attendere il successo, aspetta per sempre ...
Michael Rueegg

5

A questo scopo userei un attore Akka. A differenza della comprensione, fallisce non appena uno dei futuri fallisce, quindi è un po 'più efficiente in questo senso.

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

Quindi, crea l'attore, inviagli un messaggio (in modo che sappia a dove inviare la sua risposta) e attendi una risposta.

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
}

Sembra un po 'troppo complesso per un compito così semplice. Ho davvero bisogno di un attore che aspetti solo il futuro? Grazie comunque.
Michael

1
Non sono riuscito a trovare alcun metodo adatto nell'API che possa fare esattamente quello che vuoi, ma forse mi sono perso qualcosa.
Robin Green

5

Questa domanda è stata risolta, ma sto pubblicando la mia soluzione di classi di valore (le classi di valore sono state aggiunte in 2.10) poiché non ce n'è una qui. Sentiti libero di criticare.

  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 è un wrapper Future senza overhead che cambia la mappa / flatMap Future predefinita da fai-questo-allora-quello a combina-tutto-e-fallisci-se-qualsiasi-fallisce. Utilizzo:

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") }

Nell'esempio sopra, f1, f2 e f3 verranno eseguiti contemporaneamente e se qualcuno fallisce in qualsiasi ordine, il futuro della tupla fallirà immediatamente.


Eccezionale! Qualche lib che fornisce quel tipo di funzione di utilità?
srirachapills

1
Sì, da allora ho creato un'ampia libreria di utilità Future: github.com/S-Mach/s_mach.concurrent Vedi async.par nel codice di esempio.
lancegatlin


2

Puoi usare questo:

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")
  }
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.