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

时间:2013-09-10 08:59:22

标签: scala nonblocking future scala-2.10

我试图理解声明背后的理性  For cases where blocking is absolutely necessary, futures can be blocked on (although it is discouraged)

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

2 个答案:

答案 0 :(得分:11)

Futures的大多数是它们使您能够创建可以轻松并行执行的非阻塞并发代码。

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

另一方面,如果你注册了一个回调(例如使用 onComplete onSuccess )并在回调中输入关注结果的代码,那么你可以有代码,可以组织起来非常有效地运行和扩展。它变成事件驱动而不是必须等待结果。

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

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

你可以从那里进步到

  1. 创建一个对象来存储结果。
  2. 为每个将结果插入对象的未来注册回调。
  3. 启动 n 计算
  4. 然后对实际结果到达所需的时间进行基准测试,当您全部要求它们时。那会更有意义。

    修改

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

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

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

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

答案 1 :(得分:6)

ForkJoinTask报告的Java7文档:

  

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

Doug Lea的JSR166 maintenance repository(针对JDK8)扩展了这一点:

  

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

     

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

TL;博士;

&#34;阻止加入&#34; fork-join引用的操作不要与调用某些&#34;阻塞代码&#34;在任务中。

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

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

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