Як чекати декількох ф'ючерсів?


86

Припустимо, у мене є кілька ф’ючерсів і мені потрібно почекати, поки будь- яке з них зазнає невдачі або всі вони досягнуть успіху.

Наприклад: Нехай є 3 ф'ючерсів: f1, f2, f3.

  • Якщо це f1вдається і f2не вдається, я не чекаю f3(і повертаю помилку клієнту).

  • Якщо f2не вдається, f1і f3все ще працює, я їх не чекаю (і повертаю помилку )

  • Якщо f1вдасться, а потім f2вдасться, я продовжую чекати f3.

Як би ви це реалізували?


випуск Scala щодо цього питання. issues.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в такому випадку? Якщо ми будемо, це не зовсім те, що я хочу.
Майкл

Але якщо 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
}

Тоді ви можете 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
Приємна реалізація. Але зауважте, що якщо ви передасте порожню послідовність ф’ючерсів на awaitSuccess, це чекає вічно ...
Michael Rueegg

5

Для цієї мети я використав би актора Акка. На відміну від розуміння, воно зазнає невдачі, як тільки будь-яке майбутнє зазнає краху, тому в цьому сенсі воно трохи ефективніше.

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
}

Виглядає занадто складно для такого простого завдання. Мені справді потрібен актор, щоб просто чекати майбутнього? Все одно, дякую.
Майкл

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 - це оболонка майбутнього обгортки, яка змінює стандартну карту майбутнього / 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 працюватимуть одночасно, і якщо якісь збої в будь-якому порядку, майбутнє кортежу негайно вийде з ладу.


Чудово! Будь-яка бібліотека, яка забезпечує такий вид функцій корисності?
srirachapills

1
Так, з тих пір я створив розширену утиліту Future lib: 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")
  }
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.