Как предотвратить актерское голодание в присутствии других долгоиграющих актеров? - PullRequest
5 голосов
/ 08 ноября 2010

Это использует Scala 2.8 Актеры.У меня долгая работа, которую можно распараллелить.Он состоит из около 650 000 единиц работы.Я делю его на 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 + ч.Проблема заключается в том, что в то же время любые другие действующие лица, которых я создаю для выполнения обычных задач, кажутся голодными первоначальными 2600 действующими лицами, которые также терпеливо ожидают своего времени, чтобы быть запущенным в потоке, но ждут дольше, чем любые новые действующие лица, которыедавай.

Как мне избежать этого голода?

Начальные мысли:

  • Вместо 2600 актеров, используйте одного актера, который последовательно пропускает через большойкуча работы.Мне это не нравится, потому что я бы хотел, чтобы эта работа была закончена раньше, разделив ее на части.
  • Вместо 2600 актеров используйте двух актеров, каждый из которых обрабатывает свою половину всего рабочего набора.Это может работать лучше, но что, если моя машина имеет 8 ядер?Я, вероятно, хотел бы использовать больше, чем это.

ОБНОВЛЕНИЕ

Некоторые люди вообще ставили под сомнение использование Актеров,тем более, что возможность передачи сообщений не использовалась внутри рабочих.Я предполагал, что 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)

Я использовал абстракцию Future в ActorTest, чтобы избежать передачи сообщения другому актеру, чтобы показать, что работа выполнена.Я был удивлен, обнаружив, что мой код актера был в 10 раз медленнее.Обратите внимание, что я также создал свой ThreadPoolExecutor с начальным размером пула, с которым создается пул Actor по умолчанию.

Оглядываясь назад, мне кажется, что я, возможно, злоупотребил абстракцией Actor.Я собираюсь изучить использование отдельных ThreadPools для этих отдельных, дорогостоящих и длительных задач.

Ответы [ 3 ]

6 голосов
/ 09 ноября 2010

Независимо от того, сколько у вас есть действующих лиц, если вы не настраиваете свое планирование явно, все они поддерживаются планировщиком single fork / join (работающим в пуле потоков с емкостью 4, еслиЯ не ошибаюсь).Вот откуда приходит голод.

  1. Вы должны попробовать разные планировщики для вашего пула актеров, чтобы найти тот, который показывает лучшую производительность (попробуйте ResizableThreadPoolScheduler, если вы хотите максимизировать параллелизм, используя столько потоков, скольковозможно)
  2. Вам нужен отдельный планировщик для огромного количества участников (другие акторы в вашей системе не должны его использовать)
  3. Как и было предложено @DaGGeRRz, вы можете попробовать Akkaинфраструктура, которая предлагает настраиваемые диспетчеры (например, диспетчер балансировки нагрузки при краже рабочей нагрузки перемещает события из почтовых ящиков занятых актеров в свободных актеров)

Из комментариев по умолчанию Актер реализация:

Систему выполнения можно настроить на использование большего размера пула потоков (например, установив свойство actors.corePoolSize JVM).Метод scheduler признака Actor может быть переопределен, чтобы возвращать ResizableThreadPoolScheduler, который изменяет размер своего пула потоков, чтобы избежать голодания, вызванного субъектами, которые вызывают произвольные методы блокировки.Свойство actors.enableForkJoin JVM может быть установлено в false, и в этом случае ResizableThreadPoolScheduler используется по умолчанию для выполнения акторов.

Кроме того: интересный поток в планировщиках в Скала-Ланге.

4 голосов
/ 09 ноября 2010

Из вашего примера видно, что вам вообще не нужно использовать актеров, поскольку вы не передаете сообщения своим рабочим подразделениям, не отвечаете и даже не зацикливаетесь.

Почему бы просто не создатьнагрузка Future с, а потом ждать их окончания?Таким образом, базовый пул Fork Join полностью свободен в выборе подходящего уровня параллелизма (т. Е. Количества потоков) для вашей системы:

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

Обратите внимание, что вы получаете только преимущество из параллелизма путем одновременной обработки большего количества задач, чем у вашей системы, если дорогая работа не связана с процессором (возможно, она связана с вводом-выводом).

3 голосов
/ 08 ноября 2010

Я не использовал актеров с таким синтаксисом, но по умолчанию я думаю, что все акторы в Scala используют пул потоков.

См. Как определить пул потоков для актеров

...