About Future.firstCompletedOf and Garbage Collect mechanism

后端 未结 1 1126
误落风尘
误落风尘 2021-02-20 09:28

I\'ve encountered this problem in my real-life project and proved by my testing code and profiler. Instead of pasting \"tl;dr\" code, I\'m showing you a picture and then describ

1条回答
  •  难免孤独
    2021-02-20 09:43

    Let's see how firstCompletedOf is implemented:

    def firstCompletedOf[T](futures: TraversableOnce[Future[T]])(implicit executor: ExecutionContext): Future[T] = {
      val p = Promise[T]()
      val completeFirst: Try[T] => Unit = p tryComplete _
      futures foreach { _ onComplete completeFirst }
      p.future
    }
    

    When doing { futures foreach { _ onComplete completeFirst }, the function completeFirst is saved somewhere via ExecutionContext.execute. Where exactly is this function saved is irrelevant, we just know that it has to be saved somewhere so that it can be picked later on and executed on a thread pool when a thread becomes available. Only when the future has completed is the reference to completeFirst not needed anymore.

    Because completeFirst closes over p, as long as there is still one future (from futures) waiting to be completed there is a reference to p that prevents it to be garbage collected (even though by that point chances are that firstCompletedOf has already returned, removing p from the stack).

    When the first future completes, it saves the result into the promise (by calling p.tryComplete). Because the promise p holds the result, the result is reachable for at least as long as p is reachable, and as we saw p is reachable as long as at least one future from futures has not completed. This is the reason why the result cannot be collected before all the futures have completed.

    UPDATE: Now the question is: could it be fixed? I think it could. All we would have to do is to ensure that the first future to complete "nulls out" the reference to p in a thread-safe way, which can be done by example using an AtomicReference. Something like this:

    def firstCompletedOf[T](futures: TraversableOnce[Future[T]])(implicit executor: ExecutionContext): Future[T] = {
      val p = Promise[T]()
      val pref = new java.util.concurrent.atomic.AtomicReference(p)
      val completeFirst: Try[T] => Unit = { result: Try[T] =>
        val promise = pref.getAndSet(null)
        if (promise != null) {
          promise.tryComplete(result)
        }
      }
      futures foreach { _ onComplete completeFirst }
      p.future
    }
    

    I have tested it and as expected it does allow the result to be garbage collected as soon as the first future completes. It should behave the same in all other respects.

    0 讨论(0)
提交回复
热议问题