Làm thế nào để chờ đợi một số Hợp đồng tương lai?


86

Giả sử tôi có một số tương lai và cần đợi cho đến khi một trong hai tương lai thất bại hoặc tất cả chúng đều thành công.

Ví dụ: Hãy có 3 kỳ hạn: f1, f2, f3.

  • Nếu f1thành công và f2không thành công, tôi không chờ đợi f3(và trả lại lỗi cho khách hàng).

  • Nếu f2không thành công trong khi f1f3vẫn đang chạy, tôi không đợi chúng (và trả lại lỗi )

  • Nếu f1thành công và sau đó f2thành công tôi tiếp tục chờ đợi f3.

Bạn sẽ thực hiện nó như thế nào?


một vấn đề Scala về câu hỏi này. problem.scala-lang.org/browse/SI-8994 API phải có tùy chọn cho các hành vi khác nhau
WeiChing 林 煒 清

Câu trả lời:


83

Thay vào đó, bạn có thể sử dụng cách hiểu như sau:

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

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

Trong ví dụ này, hợp đồng tương lai 1, 2 và 3 được bắt đầu song song. Sau đó, trong phần đọc hiểu, chúng ta đợi cho đến khi có kết quả 1 rồi đến 2 và 3. Nếu 1 hoặc 2 không thành công, chúng tôi sẽ không đợi 3 nữa. Nếu cả 3 đều thành công, thì aggFutval sẽ giữ một bộ giá có 3 chỗ trống, tương ứng với kết quả của 3 tương lai.

Bây giờ, nếu bạn cần hành vi mà bạn muốn dừng chờ đợi nếu trước tiên nói rằng fut2 không thành công, mọi thứ sẽ phức tạp hơn một chút. Trong ví dụ trên, bạn sẽ phải đợi fut1 hoàn thành trước khi nhận ra rằng fut2 không thành công. Để giải quyết điều đó, bạn có thể thử một cái gì đó như sau:

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

Bây giờ điều này hoạt động chính xác, nhưng vấn đề đến từ việc biết cái nào Futuređể loại bỏ Mapkhi một cái đã được hoàn thành thành công. Miễn là bạn có một số cách để tương quan đúng một kết quả với Tương lai đã tạo ra kết quả đó, thì điều gì đó tương tự sẽ hoạt động. Nó chỉ đệ quy tiếp tục xóa các Hợp đồng tương lai đã hoàn thành khỏi Bản đồ và sau đó gọi Future.firstCompletedOfphần còn lại Futurescho đến khi không còn lại, thu thập kết quả trên đường đi. Nó không đẹp, nhưng nếu bạn thực sự cần hành vi mà bạn đang nói, thì điều này hoặc thứ gì đó tương tự có thể hoạt động.


Cảm ơn bạn. Điều gì xảy ra nếu fut2thất bại trước đó fut1? Chúng ta sẽ vẫn chờ đợi fut1trong trường hợp đó chứ? Nếu chúng tôi muốn, nó không chính xác như những gì tôi muốn.
Michael

Nhưng nếu 3 thất bại trước, chúng tôi vẫn đợi 1 và 2 khi có thể về sớm. Bất kỳ cách nào để làm điều này mà không yêu cầu trình tự các tương lai?
The Archetypal Paul,

Bạn có thể cài đặt một onFailurehandler cho fut2thất bại nhanh chóng, và một onSuccesstrên aggFutđể xử lý thành công. Thành công trên aggFutngụ ý fut2đã hoàn tất thành công, vì vậy bạn chỉ có một trong các trình xử lý được gọi.
Pagoda_5b,

Tôi đã thêm một chút vào câu trả lời của mình để chỉ ra một giải pháp khả thi cho việc thất bại nhanh chóng nếu bất kỳ tương lai nào thất bại.
cmbaxter

1
Trong ví dụ đầu tiên của bạn, 1 2 và 3 không chạy song song, sau đó chạy nối tiếp. Hãy thử nó với printlines và xem
bwawok

35

Bạn có thể sử dụng một lời hứa và gửi cho nó thất bại đầu tiên hoặc thành công tổng hợp hoàn thành cuối cùng:

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
}

Sau đó, bạn có thể Awaitdựa trên kết quả đó Futurenếu bạn muốn chặn, hoặc chỉ mapnó vào một cái gì đó khác.

Sự khác biệt với phần đọc hiểu là ở đây bạn gặp lỗi đầu tiên không thành công, trong khi với phần đọc hiểu bạn gặp lỗi đầu tiên theo thứ tự duyệt của tập hợp đầu vào (ngay cả khi lỗi khác đầu tiên bị lỗi). Ví dụ:

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

Và:

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

Đây là một giải pháp mà không cần sử dụng diễn viên.

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

Bạn có thể làm điều này với tương lai một mình. Đây là một cách triển khai. Lưu ý rằng nó sẽ không kết thúc quá trình thực thi sớm! Trong trường hợp đó, bạn cần phải làm điều gì đó phức tạp hơn (và có thể tự thực hiện việc gián đoạn). Nhưng nếu bạn không muốn tiếp tục chờ đợi điều gì đó không hoạt động, thì điều quan trọng là tiếp tục đợi việc đầu tiên kết thúc và dừng lại khi không còn gì hoặc bạn gặp phải một ngoại lệ:

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

Dưới đây là một ví dụ về nó hoạt động khi mọi thứ hoạt động ổn:

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

Nhưng khi có sự cố xảy ra:

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
Thực hiện tốt. Nhưng lưu ý rằng nếu bạn vượt qua một chuỗi rỗng của tương lai để awaitSuccess, nó chờ đợi mãi mãi ...
Michael Rueegg

5

Vì mục đích này, tôi sẽ sử dụng một diễn viên Akka. Không giống như để hiểu, nó sẽ thất bại ngay khi bất kỳ tương lai nào thất bại, vì vậy theo nghĩa đó, nó hiệu quả hơn một chút.

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

Sau đó, tạo tác nhân, gửi tin nhắn đến nó (để nó biết nơi gửi câu trả lời của mình) và chờ trả lời.

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
}

Trông hơi phức tạp cho một nhiệm vụ đơn giản như vậy. Tôi có thực sự cần một diễn viên chỉ để chờ đợi tương lai? Dẫu sao cũng xin cảm ơn.
Michael

1
Tôi không thể tìm thấy bất kỳ phương pháp phù hợp nào trong API có thể thực hiện chính xác những gì bạn muốn, nhưng có lẽ tôi đã bỏ lỡ điều gì đó.
Robin Green

5

Câu hỏi này đã được trả lời nhưng tôi đang đăng giải pháp lớp giá trị của mình (các lớp giá trị đã được thêm vào trong 2.10) vì không có giải pháp nào ở đây. Xin vui lòng chỉ trích.

  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 là một trình bao bọc Tương lai không có chi phí nào thay đổi bản đồ Tương lai / flatMap mặc định từ do-this-then-that thành kết hợp-all-and-fail-if-any-fail. Sử dụng:

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

Trong ví dụ trên, f1, f2 và f3 sẽ chạy đồng thời và nếu bất kỳ lỗi nào theo bất kỳ thứ tự nào thì tương lai của bộ tuple sẽ bị lỗi ngay lập tức.


Tuyệt vời! Bất kỳ lib cung cấp loại chức năng tiện ích?
srirachapills

1
Có, tôi đã tạo một tiện ích mở rộng trong tương lai lib: github.com/S-Mach/s_mach.concurrent Xem async.par trong mã ví dụ.
lancegatlin


2

Bạn có thể sử dụng cái này:

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")
  }
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.