斯卡拉:加入/等待期货的增长排队

时间:2017-03-09 12:56:06

标签: multithreading scala future scala.js

我启动了几个异步进程,反过来,如果需要,可以启动更多进程(想想遍历目录结构或类似的东西)。每个进程都会返回一些内容,最后我想等待所有进程完成并安排一个函数来处理结果集合。

天真的尝试

我的解决方案尝试使用了一个可变的ListBuffer(我不断添加我生成的期货)和Future.sequence来安排一些函数在完成此缓冲区中列出的所有这些期货时运行。< / p>

我准备了一个说明问题的最小例子:

object FuturesTest extends App {
  var queue = ListBuffer[Future[Int]]()

  val f1 = Future {
    Thread.sleep(1000)
    val f3 = Future {
      Thread.sleep(2000)
      Console.println(s"f3: 1+2=3 sec; queue = $queue")
      3
    }
    queue += f3
    Console.println(s"f1: 1 sec; queue = $queue")
    1
  }
  val f2 = Future {
    Thread.sleep(2000)
    Console.println(s"f2: 2 sec; queue = $queue")
    2
  }

  queue += f1
  queue += f2
  Console.println(s"starting; queue = $queue")

  Future.sequence(queue).foreach(
    (all) => Console.println(s"Future.sequence finished with $all")
  )

  Thread.sleep(5000) // simulates app being alive later
}

它首先安排f1f2个期货,然后{1}}将在1秒之后以f3分辨率安排。f1f3本身将在2秒内解决。因此,我期望获得以下内容:

starting; queue = ListBuffer(Future(<not completed>), Future(<not completed>))
f1: 1 sec; queue = ListBuffer(Future(<not completed>), Future(<not completed>), Future(<not completed>))
f2: 2 sec; queue = ListBuffer(Future(Success(1)), Future(<not completed>), Future(<not completed>))
f3: 1+2=3 sec; queue = ListBuffer(Future(Success(1)), Future(Success(2)), Future(<not completed>))
Future.sequence finished with ListBuffer(1, 2, 3)

然而,我实际上得到了:

starting; queue = ListBuffer(Future(<not completed>), Future(<not completed>))
f1: 1 sec; queue = ListBuffer(Future(<not completed>), Future(<not completed>), Future(<not completed>))
f2: 2 sec; queue = ListBuffer(Future(Success(1)), Future(<not completed>), Future(<not completed>))
Future.sequence finished with ListBuffer(1, 2)
f3: 1+2=3 sec; queue = ListBuffer(Future(Success(1)), Future(Success(2)), Future(<not completed>))

...这很可能是因为我们等待的期货清单在Future.sequence的初始调用期间是固定的,并且不会在以后更改。

工作,但丑陋的尝试

最终,我已经按照我想要的代码行事:

  waitForSequence(queue, (all: ListBuffer[Int]) => Console.println(s"finished with $all"))

  def waitForSequence[T](queue: ListBuffer[Future[T]], act: (ListBuffer[T] => Unit)): Unit = {
    val seq = Future.sequence(queue)
    seq.onComplete {
      case Success(res) =>
        if (res.size < queue.size) {
          Console.println("... still waiting for tasks")
          waitForSequence(queue, act)
        } else {
          act(res)
        }
      case Failure(exc) =>
        throw exc
    }
  }

这符合预期,最终得到所有3个未来:

starting; queue = ListBuffer(Future(<not completed>), Future(<not completed>))
f1: 1 sec; queue = ListBuffer(Future(<not completed>), Future(<not completed>), Future(<not completed>))
f2: 2 sec; queue = ListBuffer(Future(Success(1)), Future(<not completed>), Future(<not completed>))
... still waiting for tasks
f3: 1+2=3 sec; queue = ListBuffer(Future(Success(1)), Future(Success(2)), Future(<not completed>))
finished with ListBuffer(1, 2, 3)

但它仍然非常难看。它只是重新启动Future.sequence等待,如果它看到在完成时队列长于结果数,希望下次完成时情况会更好。当然,这很糟糕,因为它会耗尽堆栈,如果这个检查会在创建未来和将其附加到队列之间的一个小窗口中触发,则可能容易出错。

是否可以不用Akka重写所有内容,或者使用Await.result(由于我的代码是为Scala.js编译而I can't actually use)。

3 个答案:

答案 0 :(得分:1)

这样做的正确方法可能是撰写期货。具体来说,f1不应该只是启动f3,它应该可能是平面映射 - 也就是说,f1的未来在f3结算之前不会解决。

请注意,Future.sequence是一种后备选项,仅在Futures全部断开连接时使用。在您所描述的情况下,如果存在真正的依赖关系,那么最好在您实际返回的期货中进行表示。使用Futures时,flatMap是您的朋友,应该是您可以使用的第一个工具之一。 (通常但不总是for理解。)

可以肯定地说,如果你想要一个可变的期货队列,代码没有正确的结构,并且有更好的方法。特别是在Scala.js中(这是我的大部分代码所在,而且非常重要),我用来理解那些期货经常 - 我认为它是唯一的理智的操作方式......

答案 1 :(得分:1)

就像Justin提到的那样,你不能失去对其他期货内部产生的期货的引用,你应该使用map和flatMap来链接它们。

val f1 = Future {
  Thread.sleep(1000)
  val f3 = Future {
    Thread.sleep(2000)
    Console.println(s"f3: 1+2=3 sec")
    3
  }
  f3.map{
    r =>
      Console.println(s"f1: 1 sec;")
      Seq(1, r)
  }
}.flatMap(identity)

val f2 = Future {
  Thread.sleep(2000)
  Console.println(s"f2: 2 sec;")
  Seq(2)
}

val futures = Seq(f1, f2)

Future.sequence(futures).foreach(
  (all) => Console.println(s"Future.sequence finished with ${all.flatten}")
)

Thread.sleep(5000) // simulates app being alive later

这适用于最小的例子,我不确定它是否适用于您的真实用例。结果是:

f2: 2 sec;
f3: 1+2=3 sec
f1: 1 sec;
Future.sequence finished with List(1, 3, 2)

答案 2 :(得分:1)

我不会涉及Future.sequence:它使操作并行化,并且您似乎正在寻找顺序的异步执行。此外,您可能不需要在定义后立即开始期货。组成看起来应该像这样:

def run[T](queue: List[() => Future[T]]): Future[List[T]] = {
  (Future.successful(List.empty[T]) /: queue)(case (f1, f2) =>
  f1() flatMap (h => )
  )

val t0 = now

def f(n: Int): () => Future[String] = () => {
  println(s"starting $n")
  Future[String] {
    Thread.sleep(100*n)
    s"<<$n/${now - t0}>>"
  }
}

println(Await.result(run(f(7)::f(10)::f(20)::f(3)::Nil), 20 seconds))

诀窍是不要过早推出期货;这就是为什么我们只有f(n)才能启动()的原因。