如何等待几个期货?


86

假设我有几个期货,需要等到其中任何一个失败全部成功。

例如:让我们有3个期货:f1f2f3

  • 如果f1成功f2失败,我就不会等待f3(并将失败返回给客户端)。

  • 如果f2在运行期间失败f1并且f3仍在运行,则不要等待它们(并返回failure

  • 如果f1成功,然后f2成功,我继续等待f3

您将如何实施?


有关此问题的Scala问题。issues.scala-lang.org/browse/SI-8994 API应该具有针对不同行为的选项
WeiChing林炜清

Answers:


83

您可以改为使用以下理解:

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

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

在此示例中,期货1、2和3并行启动。然后,为了理解,我们等到结果1,然后2,然后3可用。如果1或2失败,我们将不再等待3。如果所有3个都成功,则aggFutval将持有一个带有3个空位的元组,对应于3个期货的结果。

现在,如果您需要在首先说fut2失败的情况下想要停止等待的行为,则事情会变得有些棘手。在上面的示例中,您将必须等待fut1完成才能实现fut2失败。为了解决这个问题,您可以尝试如下操作:

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

现在,此方法可以正常工作,但是问题出在知道成功完成操作时Future要删除的内容Map。只要您有某种方法可以将结果与产生该结果的Future正确关联,那么类似的方法就可以工作。它只是递归地不断从地图中删除完整的期货,然后调用Future.firstCompletedOf剩余的Futures直到没有剩余的为止,并一路收集结果。这不是很漂亮,但是如果您确实需要您正在谈论的行为,那么此操作或类似方法都可以使用。


谢谢。如果fut2之前失败fut1怎么办?fut1在这种情况下,我们还会等待吗?如果我们愿意,那不是我想要的。
迈克尔

但是,如果3首先失败,我们仍然会等待1和2,直到我们可以早日返回。有什么方法可以执行而无需对期货进行排序?
原型保罗

您可以安装onFailure处理程序fut2以快速失败,然后继续onSuccess安装aggFut以处理成功。成功的成功aggFut意味着成功fut2完成,因此您只有一个被调用的处理程序。
pagoda_5b

我在答案中添加了一些内容,以显示一种可能的解决方案,以在任何期货出现故障时快速失败。
cmbaxter

1
在第一个示例中,1 2和3不并行运行,然后串行运行。尝试使用打印线,看看
bwawok

35

您可以使用一个承诺,并将第一个失败或最终完成的汇总成功发送给它:

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
}

然后AwaitFuture如果您想阻止,或者只是将map其塞入其他内容,就可以对结果进行处理。

与理解的区别在于,这里您得到第一个失败的错误,而对于理解,您得到的是按输入集合的遍历顺序的第一个错误(即使另一个错误首先失败)。例如:

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

和:

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

这是不使用参与者的解决方案。

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

您可以仅凭期货进行此操作。这是一个实现。请注意,它不会提早终止执行!在这种情况下,您需要做一些更复杂的事情(并且可能自己实现中断)。但是,如果您只是不想一直等待不起作用的事情,那么关键是要一直等待第一件事完成,然后在什么都没剩下或遇到异常时停止:

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

这是一个一切正常的示例:

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

但是当出现问题时:

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
很好的实现。但是请注意,如果您将空的期货序列传递给awaitSuccess,它将永远等待...
Michael Rueegg

5

为此,我将使用Akka演员。与理解不同的是,一旦任何期货交易失败,它就会失败,因此从这种意义上讲,它的效率更高。

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

然后,创建actor,向其发送消息(这样它将知道将答复发送到何处)并等待答复。

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
}

对于这样一个简单的任务,看起来有点太复杂了。我真的需要一个演员来等待期货吗?不管怎么说,还是要谢谢你。
迈克尔

1
我在API中找不到任何可以完全满足您需要的方法,但是也许我错过了一些东西。
罗宾·格林

5

这个问题已经回答了,但是我要发布我的值类解决方案(在2.10中添加了值类),因为这里没有一个。请随时批评。

  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是一个无开销的Future包装器,它将默认的Future map / flatMap从“先做后做”更改为“如果有任何失败则全部并失败”。用法:

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

在上面的示例中,f1,f2和f3将同时运行,并且如果任何顺序出现故障,则元组的将来将立即失败。


太棒了!任何提供这种实用程序功能的库?
srirachapills,2015年

1
是的,此后我创建了一个广泛的Future实用程序库:github.com/S-Mach/s_mach.concurrent请参阅示例代码中的async.par。
lancegatlin


2

您可以使用此:

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")
  }
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.