Целлулоидная асинхронность внутри блоков 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 это. Так что происходит это, в случае по умолчанию:

  1. процесс начинается.
  2. экземпляр актера.
  3. вызов метода вызывает цикл.
  4. петли вызывает async метод.
  5. async метод добавляет задачу в почтовый ящик.
  6. почтовый ящик не вызывается, и цикл продолжается.
  7. еще один async задача добавляется в почтовый ящик.
  8. это продолжается бесконечно.

выше потому что сам метод loop является Fiber вызова, который никогда не приостанавливается ( если sleep называется! ) и поэтому дополнительная задача, добавленная в почтовый ящик, никогда не является вызовом нового Fiber. А Fiber ведет себя иначе, чем Thread. Это хороший справочный материал, обсуждающий различия:


Вопрос № 3: Celluloid и Celluloid::ZMQ поведение.

третий вопрос: почему include Celluloid ведет себя иначе, чем Celluloid::ZMQ ...

потому что Celluloid::ZMQ использует реактор на основе evented почтовый ящик, против Celluloid который использует почтовый ящик на основе переменных условий.

подробнее о конвейеризации и режимах выполнения:

в этом разница между двумя примерами. Если у вас есть дополнительные вопросы о том, как ведут себя эти почтовые ящики, не стесняйтесь размещать на Группа Google ... основные динамические стоящие перед вами уникальная природа GIL взаимодействуя с Fiber и Thread и Reactor поведение.

подробнее о реакторе-схеме можно прочитать здесь:

и посмотреть конкретный реактор, используемый 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 запускает все в одном потоке, используя систему очередей запланировать асинхронные задачи на потом. Он создает новые потоки только тогда, когда это необходимо.

кроме того, Целлулоид переопределяет