ruby 块内的赛璐珞异步不起作用 [英] Celluloid async inside ruby blocks does not work

查看:91
本文介绍了ruby 块内的赛璐珞异步不起作用的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

尝试在我的工作示例上实现 Celluloid async 似乎表现出奇怪的行为.

这里是我的代码

 class 不定包括赛璐珞明确运行!循环做[1].each do |i|async.on_background结尾结尾结尾def on_background把在后台运行"结尾结尾无限期.new.run!

但是当我运行上面的代码时,我从来没有看到在后台运行"

但是,如果我睡眠,代码似乎可以工作.

class 不定包括赛璐珞明确运行!循环做[1].each do |i|async.on_background结尾睡眠 0.5结尾结尾def on_background把在后台运行"结尾结尾无限期.new.run!

有什么想法吗?为什么在上述两种情况下会有如此大的差异.

谢谢.

解决方案

您的主循环控制着参与者/应用程序的线程.

您的程序所做的只是生成后台进程,但从不运行它们.您需要在循环中 sleep 纯粹是为了让后台线程引起注意.

让无条件循环产生无限的后台进程通常不是一个好主意.应该有一个延迟,或者一个条件语句放在那里......否则你只会有一个无限循环产生永远不会被调用的东西.

这样想:如果你把 puts "looping" 放在你的循环里面,而你没有看到 Running in the background ... 你会看到循环一遍又一遍.

<小时>

方法 #1:使用 everyafter 块.

解决此问题的最佳方法不是在 loop 内使用 sleep,而是使用 afterevery 块,像这样:

every(0.1) {在_背景}

或者最重要的是,如果您想确保进程在再次运行之前完全运行,请使用 after 代替:

def run_method@running ||= false除非@running@running = 真在_背景@running = 假结尾after(0.1) { run_method }结尾

<小时>

async 中使用loop 不是一个好主意,除非完成了某种流量控制,或者使用@server 等阻塞过程.接受...否则它会无缘无故地拉动 100% 的 CPU 内核.

顺便说一句,你也可以使用 now_and_every 以及 now_and_after ......这将立即运行该块,然后在达到一定数量后再次运行它你想要的时间.

使用 every 显示在此要点中:

<小时>

我认为理想的情况:

这是一个粗略但立即可用的示例:

<小时>

需要'celluloid/current'不定类包括赛璐珞间隔 = 0.5ONE_AT_A_TIME = 真def self.run!放置000a 实例化".不确定 = 新无限期运行放置000b 永远运行:"睡觉结尾定义初始化放置001a 正在初始化".@mutex = Mutex.new 如果 ONE_AT_A_TIME@running = 假放置001b 间隔:#{INTERVAL}"结尾定义运行放置002a 正在运行".除非 ONE_AT_A_TIME &&@跑步如果 ONE_AT_A_TIME@mutex.synchronize {将002b 内锁".@running = 真在_背景@running = 假}别的放置002b 无锁".在_背景结尾结尾放置002c 设置新计时器".之后(间隔){运行}结尾def on_background如果 ONE_AT_A_TIME将003 在前台运行后台处理器".别的puts "003 在后台运行"结尾结尾结尾无限期运行!放置004 应用程序结束".

如果 ONE_AT_A_TIMEtrue,这将是它的输出:

000a 实例化.001a 正在初始化.001b 间隔:0.5002a 跑步.002b 内锁.003 在前台运行后台处理器.002c 设置新定时器.000b 永远运行:002a 跑步.002b 内锁.003 在前台运行后台处理器.002c 设置新定时器.002a 跑步.002b 内锁.003 在前台运行后台处理器.002c 设置新定时器.002a 跑步.002b 内锁.003 在前台运行后台处理器.002c 设置新定时器.002a 跑步.002b 内锁.003 在前台运行后台处理器.002c 设置新定时器.002a 跑步.002b 内锁.003 在前台运行后台处理器.002c 设置新定时器.002a 跑步.002b 内锁.003 在前台运行后台处理器.002c 设置新定时器.

如果 ONE_AT_A_TIMEfalse,这将是它的输出:

000a 实例化.001a 正在初始化.001b 间隔:0.5002a 跑步.002b 无锁.003 后台运行002c 设置新定时器.000b 永远运行:002a 跑步.002b 无锁.003 后台运行002c 设置新定时器.002a 跑步.002b 无锁.003 后台运行002c 设置新定时器.002a 跑步.002b 无锁.003 后台运行002c 设置新定时器.002a 跑步.002b 无锁.003 后台运行002c 设置新定时器.002a 跑步.002b 无锁.003 后台运行002c 设置新定时器.002a 跑步.002b 无锁.003 后台运行002c 设置新定时器.

<小时>

您需要更事件化"而不是线程化"才能正确发出任务并保留范围和状态,而不是在线程/参与者之间发出命令......这就是 everyafter 块提供.除此之外,无论哪种方式都是很好的做法,即使您没有 Global Interpreter Lock 来处理,因为在您的示例中,您似乎并没有处理阻塞过程.如果你有一个阻塞进程,那么一定会有一个无限循环.但是,由于您最终会在处理一个任务之前生成无数个后台任务,因此您需要使用 sleep 就像您的问题开始时一样,或者完全使用不同的策略,并使用 everyafter 这就是 Celluloid 本身鼓励您在处理任何类型套接字上的数据时进行操作的方式.

<小时>

方法 2:使用递归方法调用.

这只是在 Google 群组中出现的.下面的示例代码实际上允许执行其他任务,即使它是一个无限循环.

这种方法不太理想,因为它可能有更多开销,产生一系列光纤.

定义工作# ...异步工作结尾

<小时>

问题 2:ThreadFiber 行为.

第二个问题是为什么以下会起作用:loop { Thread.new { puts "Hello" } }

这会产生无限数量的进程线程,它们由 RVM 直接管理.即使您正在使用的 RVM 中有一个 Global Interpreter Lock ......这仅意味着没有使用 green 线程,这是提供的由操作系统本身......相反,这些由进程本身处理.进程的 CPU 调度程序会毫不犹豫地运行每个 Thread 本身.在这个例子中,Thread 运行得非常快,然后就死了.

async 任务相比,使用了 Fiber.所以在默认情况下发生的事情是这样的:

  1. 流程开始.
  2. Actor 已实例化.
  3. 方法调用调用循环.
  4. 循环调用 async 方法.
  5. async 方法将任务添加到邮箱.
  6. 未调用邮箱,循环继续.
  7. 另一个 async 任务被添加到邮箱中.
  8. 这会无限延续.

以上是因为循环方法本身是一个 Fiber 调用,它永远不会被挂起(除非调用了 sleep!),因此添加了额外的任务到邮箱绝不是调用新的Fiber.FiberThread 的行为不同.这是讨论差异的很好的参考资料:

<小时>

问题 #3:CelluloidCelluloid::ZMQ 行为.

第三个问题是为什么 include CelluloidCelluloid::ZMQ 的行为不同......

那是因为 Celluloid::ZMQ 使用基于反应器的事件邮箱,而 Celluloid 使用基于条件变量的邮箱.

阅读有关流水线和执行模式的更多信息:

这就是两个例子的区别.如果您对这些邮箱的行为有其他疑问,请随时在 Google 上发帖组 ...您面临的主要动态是 GILFiberThreadThread 交互的独特性质. Reactor 行为.

您可以在此处阅读有关反应器模式的更多信息:

并在此处查看 Celluloid::ZMQ 使用的特定反应器:

那么在事件邮箱场景中发生的事情是,当 sleep 被命中时,这是一个阻塞调用,这会导致反应器移动到邮箱中的下一个任务.

而且,这对于您的情况来说是独一无二的,Celluloid::ZMQ 使用的特定反应器正在使用一个永恒的 C 库......特别是 0MQ图书馆.该反应器位于您的应用程序的外部,其行为与 Celluloid::IOCelluloid 本身不同,这也是行为发生与您预期不同的原因.>

多核支持替代

如果维护状态和范围对您来说并不重要,如果您使用不限于一个操作系统线程的 jRubyRubinius,而不是使用 MRI 具有 Global Interpreter Lock,您可以实例化多个 actor 并同时在 actor 之间发出 async 调用.

但我的拙见是使用频率非常高的计时器会更好地为您服务,例如在我的示例中为 0.0010.1,这对于所有意图和目的,但也允许actor线程有足够的时间切换纤程并在邮箱中运行其他任务.

Trying to implement Celluloid async on my working example seem to exhibit weird behavior.

here my code looks

 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!

but when I run the above code, I never see the puts "Running in Background"

But, if I put a sleep the code seem to work.

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!

Any idea? why such a difference in the above two scenario.

Thanks.

解决方案

Your main loop is dominating the actor/application's threads.

All your program is doing is spawning background processes, but never running them. You need that sleep in the loop purely to allow the background threads to get attention.

It is not usually a good idea to have an unconditional loop spawn infinite background processes like you have here. There ought to be either a delay, or a conditional statement put in there... otherwise you just have an infinite loop spawning things that never get invoked.

Think about it like this: if you put puts "looping" just inside your loop, while you do not see Running in the background ... you will see looping over and over and over.


Approach #1: Use every or after blocks.

The best way to fix this is not to use sleep inside a loop, but to use an after or every block, like this:

every(0.1) {
    on_background
}

Or best of all, if you want to make sure the process runs completely before running again, use after instead:

def run_method
    @running ||= false
    unless @running
        @running = true
        on_background
        @running = false
    end
    after(0.1) { run_method }
 end


Using a loop is not a good idea with async unless there is some kind of flow control done, or a blocking process such as with @server.accept... otherwise it will just pull 100% of the CPU core for no good reason.

By the way, you can also use now_and_every as well as now_and_after too... this would run the block right away, then run it again after the amount of time you want.

Using every is shown in this gist:


The ideal situation, in my opinion:

This is a rough but immediately usable example:


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."

This will be its output, if 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.

And this will be its output if 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.


You need to be more "evented" than "threaded" to properly issue tasks and preserve scope and state, rather than issue commands between threads/actors... which is what the every and after blocks provide. And besides that, it's good practice either way, even if you didn't have a Global Interpreter Lock to deal with, because in your example, it doesn't seem like you are dealing with a blocking process. If you had a blocking process, then by all means have an infinite loop. But since you're just going to end up spawning an infinite number of background tasks before even one is processed, you need to either use a sleep like your question started with, or use a different strategy altogether, and use every and after which is how Celluloid itself encourages you to operate when it comes to handling data on sockets of any kind.


Approach #2: Use a recursive method call.

This just came up in the Google Group. The below example code will actually allow execution of other tasks, even though it's an infinite loop.

This approach is less desirable because it will likely have more overhead, spawning a series of fibers.

def work
    # ...
    async.work
end


Question #2: Thread vs. Fiber behaviors.

The second question is why the following would work: loop { Thread.new { puts "Hello" } }

That spawns an infinite number of process threads, which are managed by the RVM directly. Even though there is a Global Interpreter Lock in the RVM you are using... that only means no green threads are used, which are provided by the operating system itself... instead these are handled by the process itself. The CPU scheduler for the process runs each Thread itself, without hesitation. And in the case of the example, the Thread runs very quickly and then dies.

Compared to an async task, a Fiber is used. So what's happening is this, in the default case:

  1. Process starts.
  2. Actor instantiated.
  3. Method call invokes loop.
  4. Loop invokes async method.
  5. async method adds task to mailbox.
  6. Mailbox is not invoked, and loop continues.
  7. Another async task is added to the mailbox.
  8. This continues infinitely.

The above is because the loop method itself is a Fiber call, which is not ever being suspended ( unless a sleep is called! ) and therefore the additional task added to the mailbox is never an invoking a new Fiber. A Fiber behaves differently than a Thread. This is a good piece of reference material discussing the differences:


Question #3: Celluloid vs. Celluloid::ZMQ behavior.

The third question is why include Celluloid behaves differently than Celluloid::ZMQ ...

That's because Celluloid::ZMQ uses a reactor-based evented mailbox, versus Celluloid which uses a condition variable based mailbox.

Read more about pipelining and execution modes:

That is the difference between the two examples. If you have additional questions about how these mailboxes behave, feel free to post on the Google Group ... the main dynamic you are facing is the unique nature of the GIL interacting with the Fiber vs. Thread vs. Reactor behavior.

You can read more about the reactor-pattern here:

And see the specific reactor used by Celluloid::ZMQ here:

So what's happening in the evented mailbox scenario, is that when sleep is hit, that is a blocking call, which causes the reactor to move to the next task in the mailbox.

But also, and this is unique to your situation, the specific reactor being used by Celluloid::ZMQ is using an eternal C library... specifically the 0MQ library. That reactor is external to your application, which behaves differently than Celluloid::IO or Celluloid itself, and that is also why the behavior is occurring differently than you expected.

Multi-core Support Alternative

If maintaining state and scope is not important to you, if you use jRuby or Rubinius which are not limited to one operating system thread, versus using MRI which has the Global Interpreter Lock, you can instantiate more than one actor and issue async calls between actors concurrently.

But my humble opinion is that you would be much better served using a very high frequency timer, such as 0.001 or 0.1 in my example, which will seem instantaneous for all intents and purposes, but also allow the actor thread plenty of time to switch fibers and run other tasks in the mailbox.

这篇关于ruby 块内的赛璐珞异步不起作用的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆