จะรอ Futures หลาย ๆ ตัวได้อย่างไร?


86

สมมติว่าฉันมีอนาคตหลายอย่างและต้องรอจนกว่าสิ่งใดสิ่งหนึ่งจะล้มเหลวหรือประสบความสำเร็จทั้งหมด

ตัวอย่างเช่น: Let มี 3 f1ฟิวเจอร์ส: f2, f3,

  • หากf1ประสบความสำเร็จและf2ล้มเหลวฉันไม่รอf3(และส่งคืนความล้มเหลวให้กับลูกค้า)

  • หากf2ล้มเหลวขณะที่f1และf3ยังคงทำงานผมไม่รอให้พวกเขา (และผลตอบแทนจากความล้มเหลว )

  • หากf1ประสบความสำเร็จแล้วf2ฉันก็รอต่อf3ไป

คุณจะใช้มันอย่างไร?


ปัญหา Scala เกี่ยวกับคำถามนี้ issue.scala-lang.org/browse/SI-8994 API ควรมีตัวเลือกสำหรับพฤติกรรมที่แตกต่างกัน
WeiChing 林煒清

คำตอบ:


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 ประสบความสำเร็จaggFutวาลจะมีทูเพิล 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.firstCompletedOfสิ่งที่เหลือFuturesจนกว่าจะไม่มีเหลือรวบรวมผลลัพธ์ระหว่างทาง มันไม่สวย แต่ถ้าคุณต้องการพฤติกรรมที่คุณกำลังพูดถึงจริงๆสิ่งนี้หรือสิ่งที่คล้ายกันก็สามารถใช้ได้


ขอขอบคุณ. จะเกิดอะไรขึ้นหากfut2ล้มเหลวก่อนfut1? เราจะยังรอfut1ในกรณีนั้นหรือไม่? ถ้าเราจะไม่ตรงตามที่ฉันต้องการ
Michael

แต่ถ้า 3 ล้มเหลวก่อนเรายังคงรอ 1 และ 2 เมื่อเราสามารถกลับมาก่อนเวลาได้ วิธีใดในการทำสิ่งนี้โดยไม่ต้องจัดลำดับฟิวเจอร์ส?
The Archetypal Paul

คุณสามารถติดตั้งonFailureจัดการสำหรับfut2ที่จะล้มเหลวอย่างรวดเร็วและonSuccessในaggFutการประสบความสำเร็จที่จับ ความสำเร็จในaggFutนัยfut2สำเร็จสำเร็จดังนั้นคุณจึงมีเพียงหนึ่งในตัวจัดการที่เรียกว่า
เจดีย์ _5b

ฉันเพิ่มคำตอบอีกเล็กน้อยเพื่อแสดงวิธีแก้ปัญหาที่เป็นไปได้สำหรับการล้มเหลวอย่างรวดเร็วหากอนาคตใด ๆ ล้มเหลว
cmbaxter

1
ในตัวอย่างแรกของคุณ 1 2 และ 3 ไม่ทำงานแบบขนานจากนั้นให้รันแบบอนุกรม ลองใช้ printlines ดู
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
}

จากนั้นคุณสามารถทำAwaitผลลัพธ์นั้นได้Futureหากคุณต้องการบล็อกหรือเพียงแค่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
การใช้งานที่ดี แต่โปรดทราบว่าหากคุณส่งลำดับฟิวเจอร์สที่ว่างเปล่าเพื่อรอความสำเร็จมันจะรอตลอดไป ...
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

จากนั้นสร้างนักแสดงส่งข้อความไปหานักแสดง (เพื่อที่จะได้รู้ว่าจะต้องตอบกลับไปที่ใด) และรอการตอบกลับ

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
}

ดูซับซ้อนเกินไปสำหรับงานง่ายๆเช่นนี้ ฉันต้องการนักแสดงเพื่อรออนาคตจริงๆหรือ? ขอบคุณต่อไป
Michael

1
ฉันไม่พบวิธีการใด ๆ ที่เหมาะสมใน API ที่สามารถทำสิ่งที่คุณต้องการได้ แต่บางทีฉันอาจพลาดอะไรไป
Robin Green

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 wrapper ค่าใช้จ่ายที่เปลี่ยนแผนที่อนาคต / flatMap ที่เป็นค่าเริ่มต้นจาก do-this-then-that เพื่อรวม-all-and-fail-if-any-fail การใช้งาน:

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 จะทำงานพร้อมกันและหากล้มเหลวในลำดับใด ๆ อนาคตของทูเปิลจะล้มเหลวทันที


สุดยอด! lib ใดที่มีฟังก์ชันยูทิลิตี้ประเภทนั้นหรือไม่?
srirachapills

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")
  }
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.