在课程<Principles Of Reactive Programming>里Week3的一节 "Promises, promises, promises"中,Erik Meijer举了一个例子,实现一个函数:
def sequence[T](fs: List[Future[T]]): Future[List[T]] = {.....}
这个函数实际在Scala library的Future对象中有标准的实现。
def sequence[A, M[X] <: TraversableOnce[X]](in: M[Future[A]])(implicit cbf: CanBuildFrom[M[Future[A]], A, M[A]], executor: ExecutionContext): Future[M[A]]
Simple version of Future.traverse. Transforms a TraversableOnce[Future[A]] into a Future[TraversableOnce[A]]. Useful for reducing many Futures into a single Future.
俺就想试着自己实现一下,于是写出了下面这段有问题的代码……
object ConcurrentTool { def collect[T](futures: List[Future[T]]): Future[List[T]] = { val p = Promise[List[T]] var list = List.empty[T] futures.foldLeft(list) { (curr, future) => { if (p.isCompleted) curr else { future.onComplete { case Failure(e) => p.failure(e) case Success(e) => list = e :: list } list } } } if (!p.isCompleted) p.success(list) p.future } def main(args: Array[String]) = { def futureOne = { Future { 1 } } def futureTwo = { Future { 2 } } val collection = collect(List(futureOne, futureTwo)) val lists = Await.result(collection, 1 seconds) println(lists.size) } }
collect方法用于将一个List[Future[T]]变成一个Future[List[T]]。然后我传给它两个future,等返回的Future[List[T]] compelete,然后取结果List的大小,打印出来。
但是结果是0……但是,在我加了断点进行调试时,有时结果是2,为啥呢? 错误不可怕,这是纠正自己的机会。
再来看一下collect方法的实现吧。我用一个Promise生成最后的future, 用一个空List做为foldLeft的初始值,然后遍历List[Future]里的所有future。对于每个future,给它注册一个回调函数,当它fail的时候,用引起fail的异常去complete跟最后结果相关的那个Promise,如果这个future成功了,就把它的结果附加在list里。在注册完回调之后,我返回保存结果的list。
问题就在这些操作的执行时间上。给future注册回调函数的动作是在main线程中,这个注册不会阻塞main线程的执行,假如被注册的函数的确是在另一个线程中执行的,那么在注册完回调函数之后,我返回的list仍然可能是最初的那个empty list, 所以在foldLeft完成后,foldLeft返回的仍然可能是那个empty list。接下来,我判断p.isCompleted, 如果否,我就用这个list去complete这个Promise,实际上我用一个empty list去complete了它,所以在获取collection的结果后,发现这个是一个空列表。
那么我们想要的结果如何实现呢?关键是,必须得生成这个Future[List]的所有future都compelete时,这个Future[List]才能complete。如何实现这一点呢?
- 我们可以阻塞执行collect方法的线程,显式地等待List[Future]里的所有future完成,再complete跟结果相关的那个Promise。但是这样做,会阻塞调用collect的线程,也违背了我们返回一个Future的目的。
- 我们可以在另一个线程里等待List[Future]里的所有future完成,再complete跟结果相关的那个Promise。在collect方法中,返回Promise对应的future。这样就不会阻塞调用collect的线程。
如何在另一个线程等待呢?可以用Await来阻塞等待,或者注册callback,使得当所有future完成时,callback被调用。
假如有两个Future,可以用下边的代码注册callback。
def waitBoth[T](futureA: Future[T], futureB: Future[T]): Future[List[T]] = { val p = Promise[List[T]]() futureA.onComplete{ case Failure(e) => p.failure(e) case Success(t) => futureB.onComplete{ case Failure(eb) => p.failure(eb) case Success(b) => p.success(t :: b :: Nil) } } p.future }
这个变形一下,用flatMap和map表示就是
def waitBoth[T](futureA: Future[T], futureB: Future[T]): Future[List[T]] = { val p = Promise[List[T]]() futureA.onComplete{ case Failure(e) => p.failure(e) case Success(t) => futureA.flatMap{a: T => futureB map {b => p.success(a :: b :: Nil) } } } p.future }
实际上Future的map和flatMap在实现时也用了Promise, 上边的代码简化一下就是
def waitBoth[T](futureA: Future[T], futureB: Future[T]): Future[List[T]] = { futureA.flatMap { a: T => futureB map { b => a :: b :: Nil } } }
再把flatMap和map转成for循环表示,就是
def waitTwo[T](futureA: Future[T], futureB: Future[T]): Future[List[T]] = { for { a <- futureA b <- futureB } yield a :: b :: Nil }
那么如何组合更多的Future呢?我们来写一个方法把一个Future[T]的结果附加到Future[List[T]]中
def waitMore[T](futureA: Future[T], futures: Future[List[T]]): Future[List[T]] = { for{ a <- futureA b <- futures }yield a :: b }
然后以此为基础,就可以构造最早提到的sequence函数
def waitSome[T](futures: List[Future[T]]): Future[List[T]] = { val p = Promise[List[T]]() p.success(Nil) val init: Future[List[T]] = p.future futures.foldLeft(init){ (curr, f) => waitMore(f, curr) } }
上边代码关键在于了解到我们需要一个Future作为foldLeft的初始值,它必须是success的,且使其success的值为Nil。这个init实际上也可以用 val init: Future[List[T]] = Future{Nil}来得到。
这种形式距离<Principles Of Reactive Programming>给出的答案已经很接近了。实际上Erik Meijer给出了两个解法,其中跟这个相近的是
def sequence[T](fs: List[Future[T]]): Future[List[T]] = { val successful = Promise[List[T]] successful.success(Nil) fs.foldRight(successful.future){ (f, acc) => for{x <-f; xs <- acc} yield x :: xs } }