如何防止演员在其他长期演员面前挨饿?

时间:2010-11-08 20:22:40

标签: scala concurrency scala-2.8 actor

这是使用Scala 2.8 Actors。我有一个可以并行化的长期工作。它包括大约65万单位的工作。我将它划分为2600个不同的单独子任务,并为每个子任务创建一个新的演员:

actor {
  val range = (0L to total by limit)
  val latch = new CountDownLatch(range.length)
  range.foreach { offset =>
    actor {
      doExpensiveStuff(offset,limit)
      latch.countDown
    }
  }
  latch.await
}

这种方法效果相当不错,但总体上需要2 + h才能完成。问题在于,与此同时,我创建的任何其他演员都做正常的任务似乎被最初的2600名演员挨饿,这些演员也耐心等待他们的时间在一个线程上运行,但是等待的时间比任何新的演员要长来吧。

我怎样才能避免这种饥饿?

初步想法:

  • 而不是2600名演员,使用一个演员,顺序地完成大量的工作。我不喜欢这个,因为我希望通过拆分它来尽快完成这项工作。
  • 使用两个演员,而不是2600个演员,每个演员处理整个工作集的不同部分。这可能会更好,但如果我的机器有8个核心怎么办?我可能想要利用更多。
  

更新

有些人质疑Actors的使用,特别是因为消息传递能力没有在工作者中使用。我假设Actor是一个非常轻量级的抽象,围绕一个ThreadPool处于或接近相同的性能级别,只需手动编写基于ThreadPool的执行。所以我写了一个基准:

import testing._
import java.util.concurrent._
import actors.Futures._

val count = 100000
val poolSize = 4
val numRuns = 100

val ActorTest = new Benchmark {
  def run = {
    (1 to count).map(i => future {
      i * i
    }).foreach(_())
  }
}

val ThreadPoolTest = new Benchmark {
  def run = {
    val queue = new LinkedBlockingQueue[Runnable]
    val pool = new ThreadPoolExecutor(
          poolSize, poolSize, 1, TimeUnit.SECONDS, queue)
    val latch = new CountDownLatch(count)
    (1 to count).map(i => pool.execute(new Runnable {
      override def run = {
        i * i
        latch.countDown
      }
    }))
    latch.await
  }
}

List(ActorTest,ThreadPoolTest).map { b =>
  b.runBenchmark(numRuns).sum.toDouble / numRuns
}

// List[Double] = List(545.45, 44.35)

我在ActorTest中使用了Future抽象,以避免将消息传递回另一个actor以表明工作已完成。我惊讶地发现我的Actor代码慢了10倍。请注意,我还创建了我的ThreadPoolExecutor,其初始池大小用于创建默认的Actor池。

回过头来看,似乎我可能过度使用了Actor抽象。我将考虑使用单独的ThreadPools来完成这些不同的,昂贵的,长期运行的任务。

3 个答案:

答案 0 :(得分:6)

无论您有多少演员,如果您没有明确配置您的日程安排,所有演员都会使用分叉/加入调度程序(针对容量为4的线程池运行,如果我没错的话)。这就是饥饿的来源。

  1. 您应该为您的演员池尝试不同的调度程序,找到显示最佳性能的调度程序(如果您希望使用尽可能多的线程来最大化并行性,请尝试ResizableThreadPoolScheduler)
  2. 您需要为庞大的演员池(系统中的其他演员不使用它)设置单独的调度程序。
  3. 正如@DaGGeRRz建议的那样,您可以尝试提供可配置调度程序的Akka框架(例如,工作窃取负载平衡调度程序将事件从繁忙的actor的邮箱移动到空闲的actor)
  4. 从评论到默认Actor实施:

      

    可以配置运行时系统   使用更大的线程池大小(for   例如,通过设置   actors.corePoolSize JVM属性)。   scheduler的{​​{1}}方法   trait可以被覆盖以返回a   Actor,哪个   调整其线程池的大小以避免   演员造成的饥饿   调用任意阻塞方法。该   ResizableThreadPoolScheduler JVM属性   可以设置为actors.enableForkJoin,在这种情况下为a   使用false   默认情况下执行actor。

    另外:scala-lang有趣thread on schedulers

答案 1 :(得分:4)

从你的例子看来,你根本不需要使用演员,因为你没有将消息传递给你的工作单位,或者回复,甚至是循环。

为什么不创建一个Future的负载然后等待它们完成?这样,底层的Fork Join Pool可以完全自由地决定系统的适当并行级别(即线程数):

import actors.Futures._
def mkFuture(i : Int) = future {
  doExpensiveStuff(i, limit)
}
val fs = (1 to range by limit).map(mkFuture)
awaitAll(timeout, fs) //wait on the work all finishing

请注意,如果昂贵的工作不受CPU限制(可能是IO绑定),那么通过并行处理更多任务而不是系统具有内核,您只能从并行中获得受益

答案 2 :(得分:3)

我没有使用具有该语法的actor,但默认情况下我认为scala中的所有actor都使用线程池。

请参阅How to designate a thread pool for actors