Целлулоидная асинхронность внутри блоков ruby не работает
пытается реализовать Целлулоид асинхронные на моем рабочем примере, похоже, демонстрируют странное поведение.
вот мой код выглядит
class Indefinite
include Celluloid
def run!
loop do
[1].each do |i|
async.on_background
end
end
end
def on_background
puts "Running in background"
end
end
Indefinite.new.run!
но когда я запускаю вышеуказанный код, я никогда не вижу puts"работает в фоновом режиме"
но, если я ставлю сон код, кажется, работает.
class Indefinite
include Celluloid
def run!
loop do
[1].each do |i|
async.on_background
end
sleep 0.5
end
end
def on_background
puts "Running in background"
end
end
Indefinite.new.run!
есть идеи? почему такая разница в этих двух сценариев.
спасибо.
3 ответов
ваш основной цикл доминирует над потоками актера / приложения.
все, что делает ваша программа, это нерест фоновых процессов, но никогда не запускает их. Тебе это нужно!--6--> в цикле чисто, чтобы позволить фоновым потокам привлечь внимание.
обычно не рекомендуется иметь безусловный цикл, порождающий бесконечные фоновые процессы, как у вас здесь. Там должна быть либо задержка, либо условное заявление... иначе вы просто имейте бесконечный цикл, порождающий вещи, которые никогда не вызываются.
подумайте об этом так: если ты ставишь puts "looping"
только внутри вашего цикла, в то время как вы не видите Running in the background
... вы увидите looping
снова и снова.
Подход #1: Используйте every
или after
блоки.
лучший способ исправить это, чтобы не использовать sleep
внутри loop
, но использовать after
или every
блок, как это:
every(0.1) {
on_background
}
или лучше всего, если вы хотите убедиться, что процесс выполняется полностью перед запуском снова, используйте after
вместо:
def run_method
@running ||= false
unless @running
@running = true
on_background
@running = false
end
after(0.1) { run_method }
end
с помощью loop
не очень хорошая идея с async
если нет какого-то управления потоком или процесса блокировки, например, с @server.accept
... в противном случае он просто вытащит 100% ядра процессора без уважительной причины.
кстати, вы также можете использовать now_and_every
а также now_and_after
тоже... это бы запустить блок сразу, а затем запустить его снова после количество времени, которое вы хотите.
используя every
показано в этом gist:
идеальная ситуация, на мой взгляд:
это грубо, но сразу можно использовать пример:
require 'celluloid/current'
class Indefinite
include Celluloid
INTERVAL = 0.5
ONE_AT_A_TIME = true
def self.run!
puts "000a Instantiating."
indefinite = new
indefinite.run
puts "000b Running forever:"
sleep
end
def initialize
puts "001a Initializing."
@mutex = Mutex.new if ONE_AT_A_TIME
@running = false
puts "001b Interval: #{INTERVAL}"
end
def run
puts "002a Running."
unless ONE_AT_A_TIME && @running
if ONE_AT_A_TIME
@mutex.synchronize {
puts "002b Inside lock."
@running = true
on_background
@running = false
}
else
puts "002b Without lock."
on_background
end
end
puts "002c Setting new timer."
after(INTERVAL) { run }
end
def on_background
if ONE_AT_A_TIME
puts "003 Running background processor in foreground."
else
puts "003 Running in background"
end
end
end
Indefinite.run!
puts "004 End of application."
это будет его выход, если ONE_AT_A_TIME
is true
:
000a Instantiating.
001a Initializing.
001b Interval: 0.5
002a Running.
002b Inside lock.
003 Running background processor in foreground.
002c Setting new timer.
000b Running forever:
002a Running.
002b Inside lock.
003 Running background processor in foreground.
002c Setting new timer.
002a Running.
002b Inside lock.
003 Running background processor in foreground.
002c Setting new timer.
002a Running.
002b Inside lock.
003 Running background processor in foreground.
002c Setting new timer.
002a Running.
002b Inside lock.
003 Running background processor in foreground.
002c Setting new timer.
002a Running.
002b Inside lock.
003 Running background processor in foreground.
002c Setting new timer.
002a Running.
002b Inside lock.
003 Running background processor in foreground.
002c Setting new timer.
и это будет его выход, если ONE_AT_A_TIME
is false
:
000a Instantiating.
001a Initializing.
001b Interval: 0.5
002a Running.
002b Without lock.
003 Running in background
002c Setting new timer.
000b Running forever:
002a Running.
002b Without lock.
003 Running in background
002c Setting new timer.
002a Running.
002b Without lock.
003 Running in background
002c Setting new timer.
002a Running.
002b Without lock.
003 Running in background
002c Setting new timer.
002a Running.
002b Without lock.
003 Running in background
002c Setting new timer.
002a Running.
002b Without lock.
003 Running in background
002c Setting new timer.
002a Running.
002b Without lock.
003 Running in background
002c Setting new timer.
вам нужно быть более "evented", чем" threaded", чтобы правильно выдавать задачи и сохранять область и состояние, а не выдавать команды между потоками/субъектами... что every
и after
блоки. И кроме того, это хорошая практика в любом случае, даже если у тебя не было Global Interpreter Lock
чтобы иметь дело, потому что в вашем примере это не похоже на то, что вы имеете дело с процессом блокировки. Если у вас был блокирующий процесс, то непременно есть бесконечный цикл. Но так как вы просто собираетесь в конечном итоге нерест бесконечное количество фоновых задач, прежде чем даже один обрабатывается, вам нужно либо использовать sleep
как ваш вопрос начался с, или используйте совсем другую стратегию и используйте every
и after
как Celluloid
сам призывает вас работать, когда дело доходит до обработки данных о сокетах любого рода.
подход #2: Используйте вызов рекурсивного метода.
это только что появилось в группе Google. Приведенный ниже пример кода фактически позволит выполнять другие задачи, даже если он бесконечен петля.
этот подход менее желателен, потому что он, вероятно, будет иметь больше накладных расходов, порождая ряд волокон.
def work
# ...
async.work
end
Вопрос #2: Thread
и Fiber
поведения.
второй вопрос заключается в том, почему будет работать следующее:loop { Thread.new { puts "Hello" } }
это порождает бесконечное количество потоков процесса, которые управляются RVM
напрямую. Даже если есть Global Interpreter Lock
на RVM
вы используете... что значит green threads
используются, которые предоставляются самой операционной системой... вместо этого они обрабатываются самим процессом. Планировщик CPU для процесса работает каждый , без колебаний. И в случае примера Thread
работает очень быстро, а затем умирает.
по сравнению с async
задача, a это. Так что происходит это, в случае по умолчанию:
- процесс начинается.
- экземпляр актера.
- вызов метода вызывает цикл.
- петли вызывает
async
метод. -
async
метод добавляет задачу в почтовый ящик. - почтовый ящик не вызывается, и цикл продолжается.
- еще один
async
задача добавляется в почтовый ящик. - это продолжается бесконечно.
выше потому что сам метод loop является Fiber
вызова, который никогда не приостанавливается ( если sleep
называется! ) и поэтому дополнительная задача, добавленная в почтовый ящик, никогда не является вызовом нового Fiber
. А Fiber
ведет себя иначе, чем Thread
. Это хороший справочный материал, обсуждающий различия:
Вопрос № 3: Celluloid
и Celluloid::ZMQ
поведение.
третий вопрос: почему include Celluloid
ведет себя иначе, чем Celluloid::ZMQ
...
потому что Celluloid::ZMQ
использует реактор на основе evented почтовый ящик, против Celluloid
который использует почтовый ящик на основе переменных условий.
подробнее о конвейеризации и режимах выполнения:
в этом разница между двумя примерами. Если у вас есть дополнительные вопросы о том, как ведут себя эти почтовые ящики, не стесняйтесь размещать на Группа Google ... основные динамические стоящие перед вами уникальная природа GIL
взаимодействуя с Fiber
и Thread
и Reactor
поведение.
подробнее о реакторе-схеме можно прочитать здесь:
- http://en.wikipedia.org/wiki/Reactor_pattern
- объяснение "схемы реактора"
- в чем разница между моделью, управляемой событиями, и шаблоном реактора?
и посмотреть конкретный реактор, используемый Celluloid::ZMQ
здесь:
Итак, что происходит в сценарии evented mailbox, это когда sleep
попадает, то есть блокирующий вызов, который заставляет реактор перейти к следующей задаче в почтовом ящике.
но также, и это уникально для вашей ситуации, специфический реактор используется Celluloid::ZMQ
использует вечную библиотеку C++... в частности,0MQ
библиотека. Этот реактор является внешним для вашего приложения, которое ведет себя иначе, чем Celluloid::IO
или Celluloid
себя, и именно поэтому поведение происходит иначе, чем вы ожидали.
многоядерная альтернатива поддержки
если сохранение состояния и области не важно для вас, если вы используете jRuby
или Rubinius
которые не ограничиваются одним потоком операционной системы, по сравнению с использованием MRI
, которая имеет Global Interpreter Lock
, вы можете создать экземпляр более чем одного актера и выпустить async
звонки между участниками одновременно.
но мое скромное мнение заключается в том, что вам было бы намного лучше, используя очень высокочастотный таймер, такой как 0.001
или 0.1
в моем примере, который будет казаться мгновенным для всех намерений и целей, но также позволит потоку актера много времени переключать волокна и запускать другие задачи в почтовом ящике.
давайте сделаем эксперимент, немного изменив ваш пример (мы модифицируем его, потому что таким образом мы получаем то же самое "странное" поведение, делая вещи clearner):
class Indefinite
include Celluloid
def run!
(1..100).each do |i|
async.on_background i
end
puts "100 requests sent from #{Actor.current.object_id}"
end
def on_background(num)
(1..100000000).each {}
puts "message #{num} on #{Actor.current.object_id}"
end
end
Indefinite.new.run!
sleep
# =>
# 100 requests sent from 2084
# message 1 on 2084
# message 2 on 2084
# message 3 on 2084
# ...
вы можете запустить его на любом интерпретаторе Ruby, используя Celluloid
или Celluloid::ZMQ
результат всегда будет то же самое. Также обратите внимание, что вывод из Actor.current.object_id
одинаково в обоих методах, давая нам ключ, что мы имеем дело с одним актером в нашем эксперименте.
так что нет большая разница между рубиновыми и целлулоидными реализациями, если речь идет об этом эксперименте.
Давайте первый адрес почему этот код ведет себя таким образом?
нетрудно понять, почему это происходит. Целлулоид получает входящие запросы и сохраняет их в очереди задач для соответствующего субъекта. Обратите внимание, что наш первоначальный вызов run!
находится в верхней части очереди.
Целлулоид затем обрабатывает эти задачи, один на время. Если происходит блокирование вызова или sleep
вызова, согласно документация, следующая задача будет вызвана, не дожидаясь завершения текущей задачи.
обратите внимание, что в нашем эксперименте нет блокирующих вызовов. Это значит, что run!
метод будет выполняться от начала до конца, и только после того, как это будет сделано, каждый из on_background
вызовы будут вызываться в идеальном порядке.
и как это должно работа.
если добавить sleep
вызовите свой код, он уведомит Целлулоид, что он должен начать обработку следующей задачи в очереди. Таким образом, поведение, которое вы имеете во втором примере.
давайте теперь перейдем к части на как конструировать систему, так, что она не будет зависеть от sleep
звонки, что, по крайней мере, странно.
на самом деле есть хороший пример в проект Целлулоид-ZMQ страница. Отметить это петля:
def run
loop { async.handle_message @socket.read }
end
первое, что он делает это @socket.read
. Обратите внимание, что это блокирующая операция. Таким образом, Целлулоид будет обрабатывать следующее сообщение в очереди (если таковые имеются). Как только @socket.read
отвечает, будет сгенерирована новая задача. Но эта задача не будет выполнена до @socket.read
вызывается снова, тем самым блокируя выполнение и уведомляя Целлулоид для обработки со следующим элементом в очереди.
Вы, наверное, видите разницу с вашим примером. Вы не блокируя что-либо, тем самым не давая Целлулоиду возможности обрабатывать с помощью очереди.
как мы можем получить поведение, данное в Celluloid::ZMQ
пример?
первое (на мой взгляд, лучшее) решение - иметь фактический блокирующий вызов, например @socket.read
.
если в вашем коде нет блокирующих вызовов, и вам все еще нужно обрабатывать вещи в фоновом режиме, то вы должны рассмотреть другие механизмы, предоставляемые Celluloid
.
есть несколько вариантов с Целлулоид.
Можно использовать условия, фьючерсы, уведомления, или просто позвонив wait
/signal
на низком уровне, как в этом примере:
class Indefinite
include Celluloid
def run!
loop do
async.on_background
result = wait(:background) #=> 33
end
end
def on_background
puts "background"
# notifies waiters, that they can continue
signal(:background, 33)
end
end
Indefinite.new.run!
sleep
# ...
# background
# background
# background
# ...
используя sleep(0)
С Celluloid::ZMQ
я тоже заметил работает.rb файл, который вы упомянули в вашем комментарии. Он содержит следующий цикл:
loop { [1].each { |i| async.handle_message 'hello' } ; sleep(0) }
похоже, что он делает правильную работу. На самом деле, работает под jRuby
открывается, это утечка памяти. Чтобы сделать это еще более очевидным, попробуйте добавить вызов сна в handle_message
тело:
def handle_message(message)
sleep 0.5
puts "got message: #{message}"
end
высокое использование памяти, вероятно, связано с тем, что очередь заполняется очень быстро и не может быть обработана в данный момент времени. Это будет более проблематично, если handle_message
более трудоемкий, тогда это сейчас.
решения sleep
я скептически отношусь к решениям с sleep
. Они потенциально требуют много памяти и даже генерируют память подтеки. И не понятно, что вы должны передать в качестве параметра sleep
способ и почему.
как нити работают с целлулоидом
Celluloid не создает новый поток для каждой асинхронной задачи. Он имеет пул потоков, в котором он выполняет каждую задачу, синхронную и асинхронную. Ключевым моментом является то, что библиотека видит run!
функция как синхронная задача и выполняет ее в том же контексте, что и асинхронная задача.
по умолчанию Celluloid запускает все в одном потоке, используя систему очередей запланировать асинхронные задачи на потом. Он создает новые потоки только тогда, когда это необходимо.
кроме того, Целлулоид переопределяет