尝试在我的工作示例中实现Celluloid async 似乎表现出奇怪的行为。
这里我的代码看起来
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!
但是当我运行上面的代码时,我从未看到过贴子“在后台运行”
但是,如果我把睡眠,代码似乎有效。
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!
有什么想法吗?为什么在上述两种情况中存在这样的差异。
感谢。
答案 0 :(得分:17)
您的所有程序正在执行的是生成后台进程,但从不运行它们。你需要循环中的sleep
纯粹允许后台线程得到关注。
有一个无条件循环产生无限的后台进程通常不是一个好主意,就像你在这里一样。应该有一个延迟或一个条件语句放在那里......否则你只有一个无限循环产生永远不会被调用的东西。
这样想:如果你把puts "looping"
放在你的循环中,而你没有看到Running in the background
......你会一遍又一遍地看到looping
。< / p>
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%的CPU内核。
顺便说一下,您也可以使用now_and_every
以及now_and_after
......这会立即运行该块,然后在您需要的时间后再次运行它。
使用every
显示在这个要点中:
这是一个粗略但可立即使用的例子:
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
为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
为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.
你需要做更多的事情&#34;而不是&#34;线程&#34;正确地发布任务并保留范围和状态,而不是在线程/角色之间发出命令......这是every
和after
块提供的。除此之外,无论哪种方式,这都是一种很好的做法,即使你没有Global Interpreter Lock
来处理,因为在你的例子中,它似乎并不像你在处理阻止过程。如果你有一个阻塞过程,那么无论如何都要有一个无限循环。但是,由于您在处理之前最终会产生无限数量的后台任务,因此您需要使用sleep
,例如您的问题,或者完全使用不同的策略,使用every
和after
这就是Celluloid
本身鼓励您在处理任何类型套接字上的数据时的操作方式。
这刚刚出现在谷歌集团中。下面的示例代码实际上允许执行其他任务,即使它是一个无限循环。
这种方法不太理想,因为它可能会产生更多的开销,产生一系列纤维。
def work
# ...
async.work
end
Thread
与Fiber
行为。第二个问题是为什么以下内容可行:loop { Thread.new { puts "Hello" } }
产生无数个进程线程,由RVM
直接管理。即使您使用的Global Interpreter Lock
中有RVM
,但只表示没有使用green threads
,这是由操作系统本身提供的...而是处理这些{...}}由过程本身。进程的CPU调度程序毫不犹豫地运行每个Thread
本身。在示例的情况下,Thread
运行得非常快,然后就死了。
与async
任务相比,使用Fiber
。所以,在默认情况下,这是发生了什么:
async
方法。async
方法将任务添加到邮箱。async
任务已添加到邮箱中。以上是因为循环方法本身是一个Fiber
调用,它永远不会被挂起(除非调用sleep
!)因此添加到邮箱的附加任务永远不会是调用新的Fiber
。 Fiber
的行为与Thread
的行为不同。这是讨论差异的一篇很好的参考资料:
Celluloid
与Celluloid::ZMQ
行为。第三个问题是为什么include Celluloid
的行为与Celluloid::ZMQ
...
那是因为Celluloid::ZMQ
使用基于反应堆的事件邮箱,而Celluloid
使用基于条件变量的邮箱。
阅读有关流水线和执行模式的更多信息:
这是两个例子之间的区别。如果您对这些邮箱的行为方式有其他疑问,请随时在Google Group上发帖...您面临的主要动态是GIL
与Fiber
互动的独特性质与Thread
与Reactor
行为相对。
您可以在此处阅读有关reactor-pattern的更多信息:
在此处查看Celluloid::ZMQ
使用的特定反应堆:
因此,在事件邮箱方案中发生的事情是,当sleep
被命中时,这是一个阻塞调用,这会导致反应堆移动到邮箱中的下一个任务。
但是,这也是您的情况所特有的,Celluloid::ZMQ
使用的特定反应堆正在使用永恒的C库...特别是0MQ
库。该反应堆在您的应用程序外部,其行为与Celluloid::IO
或Celluloid
本身不同,这也是行为发生的原因与您预期的不同。
如果维护状态和范围对您不重要,如果您使用的jRuby
或Rubinius
不限于一个操作系统线程,而使用MRI
且Global Interpreter Lock
具有async
1}},您可以实例化多个actor并同时在actor之间发出0.001
个调用。
但我的拙见是,使用非常高频率的计时器(例如我的示例中的0.1
或{{1}})可以提供更好的服务,这对于所有意图和目的来说都是即时的,但是还允许演员线程有足够的时间来切换光纤并在邮箱中运行其他任务。
答案 1 :(得分:4)
让我们做一个实验,通过稍微修改你的例子(我们修改它,因为这样我们得到了相同的“怪异”行为,同时让事情变得清晰):
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
# ...
您可以使用Celluloid
或Celluloid::ZMQ
在任何Ruby解释器上运行它,结果始终将是相同的。还要注意,Actor.current.object_id
的输出在两种方法中是相同的,给我们提供线索,我们在实验中处理单个演员。
因此,只要涉及此实验,ruby和Celluloid实现之间没有太大区别。
让我们首先解决为什么此代码以这种方式运行?
不难理解为什么会这样。赛璐珞正在接收传入请求并将其保存在适当的actor的任务队列中。请注意,我们对run!
的原始调用位于队列的顶部。
赛璐珞然后一次一个地处理这些任务。如果恰好有阻塞呼叫或sleep
呼叫,根据documentation,将调用下一个任务,而不是等待当前任务完成。
请注意,在我们的实验中,没有阻止调用。这意味着,run!
方法将从开始到结束执行,并且只有在完成之后,才会以完美的顺序调用每个on_background
调用。
它应该如何运作。
如果在代码中添加sleep
调用,它将通知Celluloid,它应该开始处理队列中的下一个任务。因此,你在第二个例子中的行为。
现在让我们继续讨论如何来设计系统,这样它就不依赖sleep
次调用,这至少是奇怪的。
实际上Celluloid-ZMQ project页上有一个很好的例子。注意这个循环:
def run
loop { async.handle_message @socket.read }
end
它首先做的是@socket.read
。请注意,这是一个阻止操作。因此,Celluloid将处理队列中的下一条消息(如果有的话)。只要@socket.read
响应,就会生成一个新任务。但是在再次调用@socket.read
之前不会执行此任务,从而阻止执行,并通知Celluloid处理队列中的下一个项目。
您可能会看到与您的示例有所不同。你没有阻止任何东西,因此没有给Celluloid一个机会来处理队列。
我们如何在Celluloid::ZMQ
示例中获得行为?
第一个(在我看来,更好)解决方案是进行实际阻止调用,例如@socket.read
。
如果您的代码中没有阻止调用,并且您仍需要在后台处理事情,那么您应该考虑Celluloid
提供的其他机制。
Celluloid有几种选择。
可以使用conditions,futures,notifications或仅在低级别调用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
我还注意到您在评论中提到的working.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
方法以及为什么。
答案 2 :(得分:2)
线程如何使用赛璐珞
Celluloid没有为每个异步任务创建新线程。它有一个线程池,可以在其中运行每个任务,同步和异步任务。关键点在于库将run!
函数视为同步任务,并在与异步任务相同的上下文中执行它。
默认情况下,Celluloid在单个线程中运行所有 ,使用队列系统为以后安排异步任务。它仅在需要时才创建新线程。
除此之外,Celluloid会覆盖sleep
函数。这意味着每次在扩展sleep
类的类中调用Celluloid
时,库都会检查其池中是否存在非休眠线程。
在您的情况下,第一次调用sleep 0.5
时,它将创建一个新线程,以便在第一个线程处于休眠状态时执行队列中的异步任务。
所以在你的第一个例子中,只有一个Celluloid线程正在运行,执行循环。在第二个示例中,两个Celluloid线程正在运行,第一个执行循环并在每次迭代时休眠,另一个执行后台任务。
例如,您可以更改第一个示例以执行有限数量的迭代:
def run!
(0..100).each do
[1].each do |i|
async.on_background
end
end
puts "Done!"
end
使用此run!
函数时,您会看到Done!
在所有Running in background
之前打印,这意味着Celluloid在开始之前完成了run!
函数的执行同一个线程中的异步任务。