Акторы Scala: получение и реакция

110

Позвольте мне сначала сказать, что у меня довольно большой опыт работы с Java, но я только недавно заинтересовался функциональными языками. Недавно я начал смотреть на Scala, который кажется мне очень хорошим языком.

Однако я читал о фреймворке Scala Actor в Программе на Scala , и есть одна вещь, которую я не понимаю. В главе 30.4 говорится, что использование reactвместо receiveпозволяет повторно использовать потоки, что хорошо для производительности, поскольку потоки дороги в JVM.

Означает ли это , что, до тех пор , как я помню , чтобы позвонить reactвместо receive, я могу начать , как многие актеры , как мне нравится? Прежде чем открыть для себя Scala, я играл с Erlang, и автор книги Programming Erlang может похвастаться тем, что без особых усилий создал более 200 000 процессов. Я бы не хотел делать это с потоками Java. На какие ограничения я смотрю в Scala по сравнению с Erlang (и Java)?

Кроме того, как повторное использование этого потока работает в Scala? Предположим для простоты, что у меня только один поток. Будут ли все акторы, которых я запускаю, последовательно запускаться в этом потоке, или произойдет какое-то переключение задач? Например, если я запускаю двух участников, которые отправляют друг другу сообщения в пинг-понге, могу ли я зайти в тупик, если они начнут в одном потоке?

Согласно Программе на Scala , написать актеров для использования reactсложнее, чем с receive. Звучит правдоподобно, поскольку reactне возвращается. Однако в книге показано, как можно поместить reactвнутрь цикла, используя Actor.loop. В результате вы получите

loop {
    react {
        ...
    }
}

что, на мой взгляд, очень похоже на

while (true) {
    receive {
        ...
    }
}

который использовался ранее в книге. Тем не менее, в книге говорится, что «на практике программ потребуется как минимум несколько receive». Так что же мне здесь не хватает? Что можно receiveсделать, что reactнельзя, кроме возврата? И почему меня это волнует?

Наконец, подойдя к сути того, чего я не понимаю: в книге постоянно упоминается, как использование reactпозволяет отбросить стек вызовов для повторного использования потока. Как это работает? Почему нужно отбрасывать стек вызовов? И почему стек вызовов может быть отброшен, когда функция завершается выдачей исключения ( react), но не когда она завершается возвратом ( receive)?

У меня сложилось впечатление, что « Программирование на Scala» здесь замалчивает некоторые ключевые вопросы, что очень досадно, потому что в остальном это действительно отличная книга.

jqno
источник
2
см. также stackoverflow.com/questions/1526845/…
Сет Тисью

Ответы:

78

Во-первых, каждый ожидающий актер receiveзанимает поток. Если он никогда ничего не получит, этот поток никогда ничего не сделает. Актер reactне занимает никакого потока, пока что-то не получит. Как только он что-то получает, ему выделяется поток, и он инициализируется в нем.

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

Есть разные причины производительности, по которым вам может понадобиться тот или иной. Как вы знаете, слишком много потоков в Java - не лучшая идея. С другой стороны, поскольку вам нужно прикрепить актера к потоку, прежде чем он сможет это сделать react, это быстрее отправит receiveсообщение, чем reactоно. Поэтому, если у вас есть акторы, которые получают много сообщений, но очень мало с ними делают, дополнительная задержка reactможет сделать его слишком медленным для ваших целей.

Дэниел С. Собрал
источник
21

Ответ - «да» - если ваши акторы ничего не блокируют в вашем коде и вы используете его react, то вы можете запустить свою «параллельную» программу в одном потоке (попробуйте установить системное свойствоactors.maxPoolSize чтобы узнать).

Одна из наиболее очевидных причин, по которой необходимо отбросить стек вызовов, заключается в том, что в противном случае loopметод закончился бы на StackOverflowError. Как бы то ни было, фреймворк довольно ловко завершает a react, бросая a SuspendActorException, который улавливается циклическим кодом, который затем reactснова запускает через andThenметод.

Взгляните на mkBodyметод, Actorа затем на seqметод, чтобы увидеть, как цикл сам себя перепланировывает - ужасно умная штука!

Oxbow_lakes
источник
20

Эти утверждения о «отбрасывании стека» также сбили меня с толку на некоторое время, и я думаю, что понял это сейчас, и теперь это мое понимание. В случае «приема» в сообщении есть блокировка выделенного потока (с помощью object.wait () на мониторе), и это означает, что полный стек потока доступен и готов к продолжению с момента «ожидания» при получении сообщение. Например, если у вас был следующий код

  def a = 10;
  while (! done)  {
     receive {
        case msg =>  println("MESSAGE RECEIVED: " + msg)
     }
     println("after receive and printing a " + a)
  }

поток будет ждать в вызове приема, пока сообщение не будет получено, а затем продолжит и распечатает сообщение «после приема и печати 10» со значением «10», которое находится в кадре стека до того, как поток заблокирован.

В случае реакции такого выделенного потока нет, все тело метода реакции захватывается как закрытие и выполняется некоторым произвольным потоком на соответствующем актере, получающем сообщение. Это означает, что будут выполняться только те операторы, которые могут быть записаны как замыкание, и именно здесь в игру вступает возвращаемый тип «Nothing». Рассмотрим следующий код

  def a = 10;
  while (! done)  {
     react {
        case msg =>  println("MESSAGE RECEIVED: " + msg)
     }
     println("after react and printing a " + a) 
  }

Если бы у response был тип возвращаемого значения void, это означало бы, что допустимо иметь операторы после вызова «react» (в примере оператор println, который печатает сообщение «после реакции и печати 10»), но на самом деле это никогда не будет выполнен, так как только тело метода "реагировать" захватывается и упорядочивается для выполнения позже (по прибытии сообщения). Так как контракт response имеет тип возвращаемого значения «Nothing», после реакции не может быть никаких операторов, и поэтому нет причин для поддержания стека. В приведенном выше примере переменная «a» не должна поддерживаться, поскольку операторы после вызовов реакции не выполняются вообще. Обратите внимание, что все необходимые переменные в теле реакции уже зафиксированы как замыкание, поэтому он может выполняться нормально.

Фреймворк акторов Java Kilim фактически обслуживает стек, сохраняя стек, который разворачивается при получении сообщения.

Эшвин
источник
Спасибо, это было очень информативно. Но разве вы не имели в виду +aво фрагментах кода вместо +10?
jqno 07
Отличный ответ. Я тоже этого не понимаю.
santiagobasulto
8

Просто чтобы это было здесь:

Событийное программирование без инверсии управления

Эти документы связаны с scala api для Actor и обеспечивают теоретическую основу для реализации актора. В том числе, почему реакция может никогда не вернуться.

Hexren
источник
И вторая статья. Плохой контроль спама ... :( [ Участники, объединяющие потоки и события] [2] [2]: lamp.epfl.ch/~phaller/doc/haller07coord.pdf " Участники, объединяющие потоки и события"
Хексрен,
0

Я не работал с scala / akka, но понимаю, что существует очень существенная разница в способах планирования актеров. Akka - это просто умный пул потоков, который выполняет срезы времени для выполнения акторов ... Каждый раз, когда срез будет выполняться одним сообщением до завершения актором, в отличие от Erlang, которое может быть на одну инструкцию ?!

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

user3407472
источник