Why blocking on future considered a bad practice?

此生再无相见时 提交于 2021-02-07 14:28:30

问题


I'm trying to understand rational behind the statement For cases where blocking is absolutely necessary, futures can be blocked on (although it is discouraged)

Idea behind ForkJoinPool is to join processes which is blocking operation, and this is main implementation of executor context for futures and actors. It should be effective for blocking joins.

I wrote small benchmark and seems like old style futures(scala 2.9) are 2 times faster in this very simple scenario.

@inline
  def futureResult[T](future: Future[T]) = Await.result(future, Duration.Inf)

  @inline
  def futureOld[T](body: => T)(implicit  ctx:ExecutionContext): () => T = {
    val f = future(body)
    () => futureResult(f)
  }

  def main(args: Array[String]) {
    @volatile

    var res = 0d
    CommonUtil.timer("res1") {
      (0 until 100000).foreach {  i =>
       val f1 = futureOld(math.exp(1))
        val f2 = futureOld(math.exp(2))
        val f3 = futureOld(math.exp(3))
        res = res + f1() + f2() + f3()
      }
    }
    println("res1 = "+res)
    res = 0

    res = 0
    CommonUtil.timer("res1") {
      (0 until 100000).foreach {  i =>
        val f1 = future(math.exp(1))
        val f2 = future(math.exp(2))
        val f3 = future(math.exp(3))
        val f4 = for(r1 <- f1; r2 <- f2 ; r3 <- f3) yield r1+r2+r3
        res = res + futureResult(f4)
      }
    }
    println("res2 = "+res)
  }



start:res1
res1 - 1.683 seconds
res1 = 3019287.4850644027
start:res1
res1 - 3.179 seconds
res2 = 3019287.485058338

回答1:


Java7 docs for ForkJoinTask reports:

A ForkJoinTask is a lightweight form of Future. The efficiency of ForkJoinTasks stems from a set of restrictions (that are only partially statically enforceable) reflecting their intended use as computational tasks calculating pure functions or operating on purely isolated objects. The primary coordination mechanisms are fork(), that arranges asynchronous execution, and join(), that doesn't proceed until the task's result has been computed. Computations should avoid synchronized methods or blocks, and should minimize other blocking synchronization apart from joining other tasks or using synchronizers such as Phasers that are advertised to cooperate with fork/join scheduling. Tasks should also not perform blocking IO, and should ideally access variables that are completely independent of those accessed by other running tasks. Minor breaches of these restrictions, for example using shared output streams, may be tolerable in practice, but frequent use may result in poor performance, and the potential to indefinitely stall if the number of threads not waiting for IO or other external synchronization becomes exhausted. This usage restriction is in part enforced by not permitting checked exceptions such as IOExceptions to be thrown. However, computations may still encounter unchecked exceptions, that are rethrown to callers attempting to join them. These exceptions may additionally include RejectedExecutionException stemming from internal resource exhaustion, such as failure to allocate internal task queues. Rethrown exceptions behave in the same way as regular exceptions, but, when possible, contain stack traces (as displayed for example using ex.printStackTrace()) of both the thread that initiated the computation as well as the thread actually encountering the exception; minimally only the latter.

Doug Lea's maintenance repository for JSR166 (targeted at JDK8) expands on this:

A ForkJoinTask is a lightweight form of Future. The efficiency of ForkJoinTasks stems from a set of restrictions (that are only partially statically enforceable) reflecting their main use as computational tasks calculating pure functions or operating on purely isolated objects. The primary coordination mechanisms are fork(), that arranges asynchronous execution, and join(), that doesn't proceed until the task's result has been computed. Computations should ideally avoid synchronized methods or blocks, and should minimize other blocking synchronization apart from joining other tasks or using synchronizers such as Phasers that are advertised to cooperate with fork/join scheduling. Subdividable tasks should also not perform blocking I/O, and should ideally access variables that are completely independent of those accessed by other running tasks. These guidelines are loosely enforced by not permitting checked exceptions such as IOExceptions to be thrown. However, computations may still encounter unchecked exceptions, that are rethrown to callers attempting to join them. These exceptions may additionally include RejectedExecutionException stemming from internal resource exhaustion, such as failure to allocate internal task queues. Rethrown exceptions behave in the same way as regular exceptions, but, when possible, contain stack traces (as displayed for example using ex.printStackTrace()) of both the thread that initiated the computation as well as the thread actually encountering the exception; minimally only the latter.

It is possible to define and use ForkJoinTasks that may block, but doing do requires three further considerations: (1) Completion of few if any other tasks should be dependent on a task that blocks on external synchronization or I/O. Event-style async tasks that are never joined (for example, those subclassing CountedCompleter) often fall into this category. (2) To minimize resource impact, tasks should be small; ideally performing only the (possibly) blocking action. (3) Unless the ForkJoinPool.ManagedBlocker API is used, or the number of possibly blocked tasks is known to be less than the pool's ForkJoinPool.getParallelism() level, the pool cannot guarantee that enough threads will be available to ensure progress or good performance.

tl;dr;

The "blocking join" operation referred to by the fork-join is not to be confused with calling some "blocking code" within the task.

The first is about coordinating many independent tasks (which are not independent threads) to collect individual outcomes and evaluate an overall result.

The second is about calling a potentially long-blocking operation within the single tasks: e.g. IO operations over the network, a DB query, accessing the file system, accessing a globally synchronized object or method...

The second kind of blocking is discouraged for Scala Futures and ForkJoinTasks both. The main risk is that the thread-pool gets exhausted and is unable to complete tasks awaiting in the queue, while all available threads are busy waiting on blocking operations.




回答2:


Most of the point of Futures is that they enable you to create non-blocking, concurrent code that can easily be executed in parallel.

OK, so wrapping a potentially lengthy function in a future returns immediately, so that you can postpone worrying about the return value until you are actually interested in it. But if the part of the code which does care about the value just blocks until the result is actually available, all you gained was a way to make your code a little tidier (and you know, you can do that without futures - using futures to tidy up your code would be a code smell, I think). Unless the functions being wrapped in futures are absolutely trivial, your code is going to spend much more time blocking than evaluating other expressions.

If, on the other hand, you register a callback (e.g. using onComplete or onSuccess) and put in that callback the code which cares about the result, then you can have code which can be organised to run very efficiently and scale well. It becomes event driven rather than having to sit and wait for results.

Your benchmark is of the former type, but since you have some tiny functions there, there is very little to gain between executing them in parallel compared to in sequence. This means that you are mostly evaluating the overhead of creating and accessing the futures. Congratulations: you showed that in some circumstances 2.9 futures are faster at doing something trivial than 2.10 - something trivial which does not really play to the strengths of either version of the concept.

Try something a little more complex and demanding. I mean, you're requesting the future values almost immediately! At the very least, you could build an array of 100000 futures, then pull out their results in another loop. That would be testing something slightly meaningful. Oh, and have them compute something based on the value of i.

You could progress from there to

  1. Creating an object to store the results.
  2. Registering a callback with each future that inserts the result into the object.
  3. Launching your n calculations

And then benchmarking how long it takes for the actual results to arrive, when you demand them all. That would be rather more meaningful.

EDIT

By the way, your benchmark fails both on its own terms and in its understanding of the proper use of futures.

Firstly, you are counting the time it takes to retrieve each individual future result, but not the actual time it takes to evaluate res once all 3 futures have been created, nor the total time it takes to iterate through the loop. Also, your mathematical calculations are so trivial that you might actually be testing the penalty in the second test of a) the for comprehension and b) the fourth future in which the first three futures are wrapped.

Secondly, the only reason these sums probably add up to something roughly proportional to the overall time used is precisely because there is really no concurrency here.

I'm not trying to beat up on you, it's just that these flaws in the benchmark help illuminate the issue. A proper benchmark of the performance of different futures implementations would require very careful thought.



来源:https://stackoverflow.com/questions/18714627/why-blocking-on-future-considered-a-bad-practice

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!