为什么阻止未来被视为不良做法?

yur*_*ura 5 scala future nonblocking scala-2.10

我试图理解声明背后的理性 对于绝对需要阻止的情况,可以阻止期货(虽然不鼓励)

背后的想法ForkJoinPool是加入阻止操作的过程,这是期货和参与者的执行者上下文的主要实现.它应该有效阻止连接.

在这个非常简单的场景中,我写了一个小基准,看起来像旧式期货(scala 2.9)快2倍.

@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
Run Code Online (Sandbox Code Playgroud)

its*_*uce 11

Futures的大部分要点是它们使您能够创建可以轻松并行执行的非阻塞并发代码.

好的,所以在将来包装一个可能很长的函数会立即返回,这样你就可以推迟担心返回值,直到你真正对它感兴趣为止.但是,如果该部分代码确实关心的价值只有几个街区,直到其结果是实际可用的,你获得了所有的是一个方法,使你的代码稍微整洁(你知道,你可以做,没有期货-利用期货整理你的代码将是代码气味,我认为).除非在期货中包含的函数绝对是微不足道的,否则您的代码将比花在评估其他表达式上花费更多的时间.

另一方面,如果您注册了一个回调(例如使用onCompleteonSuccess)并在回调中放入了关注结果的代码,那么您可以将代码组织起来以便非常有效地运行并且可以很好地扩展.它变成事件驱动而不是必须等待结果.

您的基准测试属于前一种类型,但由于您在那里有一些微小的功能,因此并行执行它们之间几乎没有什么好处.这意味着您主要评估创建和访问期货的开销.恭喜你:你证明了在某些情况下2.9期货在做一些微不足道的事情上比在2.10更快 - 一些微不足道的事情并没有真正发挥任何一个版本概念的优势.

尝试一些更复杂和要求更高的东西.我的意思是,你几乎立即要求未来的价值观!至少,你可以建立一个100000期货的数组,然后在另一个循环中拉出他们的结果.这将测试一些有意义的东西.哦,让他们根据i的值计算一些东西.

你可以从那里进步到

  1. 创建一个对象来存储结果.
  2. 为每个将结果插入对象的未来注册回调.
  3. 启动您的ñ计算

然后对实际结果到达所需的时间进行基准测试,当您需要它们时.那会更有意义.

编辑

顺便说一句,您的基准测试在其自身条款和对正确使用期货的理解上都失败了.

首先,你指望它需要检索每个人的未来结果的时间,而不是需要评估的实际时间资源,一旦所有3个期货已经创建,也不是总花费的时间通过循环迭代.此外,你的数学计算是如此微不足道,以至于你可能实际上在第二次测试中测试惩罚:a)理解和b)前三个期货被包裹的第四个未来.

其次,这些总和可能加起来大致与使用的总时间成正比的唯一原因恰恰是因为这里确实没有并发性.

我不是想打败你,只是基准测试中的这些缺陷有助于解决这个问题.对不同期货实施的表现进行适当的基准测试需要非常仔细考虑.


pag*_*_5b 6

ForkJoinTask的 Java7文档报告:

ForkJoinTask是Future的轻量级形式.ForkJoinTasks的效率源于一系列限制(仅部分静态可执行),反映了它们作为计算纯函数或在纯粹孤立对象上运行的计算任务的预期用途.主要的协调机制是fork(),它安排异步执行,join(),在计算任务结果之前不会继续.计算应该避免同步的方法或块,并且应该最小化其他阻塞同步,除了加入其他任务或使用同步器,如Phasers,通告与fork/join调度协作.任务也不应该执行阻塞IO,理想情况下应该访问完全独立于其他正在运行的任务访问的变量.在实践中,例如使用共享输出流的这些限制的轻微违反可能是可容忍的,但是频繁使用可能导致性能差,并且如果不等待IO或其他外部同步的线程数量变得耗尽则可能无限期地停止.通过不允许抛出IOExceptions等已检查的异常来部分强制执行此使用限制.但是,计算可能仍会遇到未经检查的异常,这些异常会被尝试加入它们的调用者重新抛出.这些异常可能还包括源于内部资源耗尽的RejectedExecutionException,例如无法分配内部任务队列.Rethrown异常的行为方式与常规异常相同,但是,如果可能,包含启动计算的线程以及实际遇到异常的线程的堆栈跟踪(例如使用ex.printStackTrace()显示); 最低限度只有后者.

Doug Lea的JSR166 维护存储库(针对JDK8)扩展了以下内容:

ForkJoinTask是Future的轻量级形式.ForkJoinTasks的效率源于一系列限制(仅部分静态可执行),反映了它们作为计算任务计算纯函数或在纯粹孤立对象上运行的主要用途.主要的协调机制是fork(),它安排异步执行,join(),在计算任务结果之前不会继续.理想情况下,计算应该避免同步的方法或块,并且应该最小化其他阻塞同步,除了加入其他任务或使用同步器,例如Phasers,这些同步器被广告以配合fork/join调度.子分类任务也不应该执行阻塞I/O,理想情况下应该访问完全独立于其他正在运行的任务访问的变量.不允许抛出IOExceptions之类的已检查异常,因此松散地强制执行这些准则.但是,计算可能仍会遇到未经检查的异常,这些异常会被尝试加入它们的调用者重新抛出.这些异常可能还包括源于内部资源耗尽的RejectedExecutionException,例如无法分配内部任务队列.Rethrown异常的行为方式与常规异常相同,但是,如果可能,包含启动计算的线程以及实际遇到异常的线程的堆栈跟踪(例如使用ex.printStackTrace()显示); 最低限度只有后者.不允许抛出IOExceptions之类的已检查异常,因此松散地强制执行这些准则.但是,计算可能仍会遇到未经检查的异常,这些异常会被尝试加入它们的调用者重新抛出.这些异常可能还包括源于内部资源耗尽的RejectedExecutionException,例如无法分配内部任务队列.Rethrown异常的行为方式与常规异常相同,但是,如果可能,包含启动计算的线程以及实际遇到异常的线程的堆栈跟踪(例如使用ex.printStackTrace()显示); 最低限度只有后者.不允许抛出IOExceptions之类的已检查异常,因此松散地强制执行这些准则.但是,计算可能仍会遇到未经检查的异常,这些异常会被尝试加入它们的调用者重新抛出.这些异常可能还包括源于内部资源耗尽的RejectedExecutionException,例如无法分配内部任务队列.Rethrown异常的行为方式与常规异常相同,但是,如果可能,包含启动计算的线程以及实际遇到异常的线程的堆栈跟踪(例如使用ex.printStackTrace()显示); 最低限度只有后者.计算可能仍会遇到未经检查的异常,这些异常会被尝试加入它们的调用者重新抛出.这些异常可能还包括源于内部资源耗尽的RejectedExecutionException,例如无法分配内部任务队列.Rethrown异常的行为方式与常规异常相同,但是,如果可能,包含启动计算的线程以及实际遇到异常的线程的堆栈跟踪(例如使用ex.printStackTrace()显示); 最低限度只有后者.计算可能仍会遇到未经检查的异常,这些异常会被尝试加入它们的调用者重新抛出.这些异常可能还包括源于内部资源耗尽的RejectedExecutionException,例如无法分配内部任务队列.Rethrown异常的行为方式与常规异常相同,但是,如果可能,包含启动计算的线程以及实际遇到异常的线程的堆栈跟踪(例如使用ex.printStackTrace()显示); 最低限度只有后者.Rethrown异常的行为方式与常规异常相同,但是,如果可能,包含启动计算的线程以及实际遇到异常的线程的堆栈跟踪(例如使用ex.printStackTrace()显示); 最低限度只有后者.Rethrown异常的行为方式与常规异常相同,但是,如果可能,包含启动计算的线程以及实际遇到异常的线程的堆栈跟踪(例如使用ex.printStackTrace()显示); 最低限度只有后者.

可以定义和使用可能阻塞的ForkJoinTasks,但这样做需要进一步考虑:(1)如果任何其他任务应该依赖于阻止外部同步或I/O的任务,则完成很少.从未加入的事件类型异步任务(例如,那些子类化CountedCompleter)通常属于此类别.(2)为了尽量减少资源影响,任务应该很小; 理想情况下只执行(可能)阻止操作.(3)除非使用ForkJoinPool.ManagedBlocker API,或者已知可能阻塞的任务数小于池的ForkJoinPool.getParallelism()级别,否则池无法保证有足够的线程可用于确保进度或良好性能.

TL;博士;

fork-join引用的"阻塞连接"操作不应与在任务中调用某些"阻塞代码"相混淆.

第一个是协调许多独立任务(不是独立的线程)来收集个人结果并评估整体结果.

第二个是在单个任务中调用潜在的长时间阻塞操作:例如,通过网络进行IO操作,数据库查询,访问文件系统,访问全局同步的对象或方法......

对Scala FuturesForkJoinTasks两者都不鼓励第二种阻止.主要风险是线程池耗尽并且无法完成等待队列中的任务,而所有可用线程都忙于等待阻塞操作.