理解Celluloid并发性

10

以下是我的Celluloid代码。

  1. client1.rb 其中一个客户端。(我将其命名为客户端1)

  2. client2.rb 这是另外一个客户端。(命名为客户端2)

注意:

以上两个客户端之间唯一的区别是发送给服务器的文本。(分别为'client-1''client-2')

当我对这两个客户端进行测试(同时运行它们),并连接以下两个服务器(逐一)时,我发现了非常奇怪的结果

  1. server1.rb (从celluloid-zmq的README.md文件中选取的基本示例)

    使用此示例服务器与上述2个客户端进行通信会导致任务的并行执行

输出:

ruby server1.rb

Received at 04:59:39 PM and message is client-1
Going to sleep now
Received at 04:59:52 PM and message is client-2

注意:

client2.rb消息在client1.rb请求处于休眠状态时被处理。(并行标记)

  1. server2.rb

    将其作为上述两个客户端的示例服务器使用,未能导致任务的并行执行

OUTPUT

ruby server2.rb

Received at 04:55:52 PM and message is client-1
Going to sleep now
Received at 04:56:52 PM and message is client-2

注意:

client-2被要求等待60秒,因为client-1正在睡眠(60秒的睡眠)

我运行了上述测试多次,结果都一样。

有人能从上述测试结果中解释一下吗?

问题:为什么在处理其他请求之前celluloid需要等待60秒,即如在server2.rb案例中所示?

Ruby版本

ruby -v

ruby 2.1.2p95 (2014-05-08 revision 45877) [x86_64-darwin13.0]


你使用哪个 Ruby 引擎? - digitalextremist
1
你尝试过在server2.rb中不使用 DisplayMessage类吗?这也是一个不同之处。 - digitalextremist
@digitalextremist 在文章中更新了 Ruby 版本。 - Viren
2个回答

6
使用您的gists,我验证了这个问题可以在MRI 2.2.1、jRuby 1.7.21和Rubinius 2.5.8中重现... server1.rb和server2.rb之间的区别是后者使用了DisplayMessage和message类方法。请注意,保留html标记。

DisplayMessage中使用sleep超出了Celluloid范围。

当在server1.rb中使用sleep时,实际上是使用Celluloid.sleep,但是当在server2.rb中使用时,它使用的是Kernel.sleep...这会锁定Server的邮箱,直到60秒过去。这会阻止该Actor上未来的方法调用被处理,直到邮箱再次处理消息(Actor上的方法调用)。

有三种解决方法:

  • 使用 defer {}future {} 块。

  • 显式调用 Celluloid.sleep 而不是 sleep(如果没有显式调用 Celluloid.sleep,使用 sleep 将最终调用 Kernel.sleep,因为 DisplayMessage 没有像 Server 那样 include Celluloid

  • DisplayMessage.message 的内容带入 handle_message 中,如 server1.rb 中所示;或者至少将其带入 Server 中,因为它在 Celluloid 范围内,并且将使用正确的 sleep


defer {}方法:

def handle_message(message)
  defer {
    DisplayMessage.message(message)
  }
end

Celluloid.sleep方法:

class DisplayMessage
    def self.message(message)
      #de ...
      Celluloid.sleep 60
    end
end

不是真正的作用域问题,而是关于异步性。

重申一下,更深层次的问题不在于sleep的作用域...这就是为什么deferfuture是我最好的推荐。但是,我在评论中发表了一些内容,现在在这里发布:

使用deferfuture将会把导致Actor被绑定的任务推到另一个线程中。如果您使用future,则可以在任务完成后获得返回值;如果您使用defer,则可以“点火并忘记”。

但更好的方法是为倾向于被卡住的任务创建另一个Actor,并甚至将该其他Actor池化...如果deferfuture对您不起作用。

如果这个问题引起了跟进问题,我很乐意回答;我们有一个非常活跃的邮件列表和IRC频道。您慷慨的奖励是值得称赞的,但我们中有很多人会纯粹为帮助您而提供帮助。


@TheCodeArtist 你的意思是更改括号中的措辞吗?因为调用 Celluloid.sleep 是可以的,就像我的代码示例一样。我链接的 sleep 方法体确实会检测它是否从 actor 中运行,但如果在 Celluloid 范围之外进行调用,则需要显式调用它。 - digitalextremist
“which will end up calling Kernel.sleep”这句话似乎不正确。我在我的本地celluloid.rb副本中添加了日志,并发现在DisplayMessage.message()内调用Celluloid.sleep()确实会调用actor.sleep(而不是kernel.sleep() - TheCodeArtist
好的,现在非常清楚了。接下来等待问题提出者醒来并对你的回答进行加分 [+300]。 (^.^)/ 干杯。 - TheCodeArtist
@digitalextremist,据我所知,任何不属于Celluloid核心类的潜在IO都会导致Celluloid串行方式工作。 - Viren
Celluloid::IO 的目的是防止演员被绑定时,其邮箱将不会处理新请求。当演员不再被绑定时,它们将被接收。原因是演员及其邮箱在一个线程中运行,并且针对演员的每个方法调用都是一个 task,默认情况下发生在一个 fiber 中。可以使用每个自身为线程的任务,但这不是默认行为。一般来说,最好假设长时间运行的进程将会绑定演员。这就是我建议在需要时使用 deferfuture 的原因。 - digitalextremist
显示剩余7条评论

3
成功重现并解决了问题。 删除之前的答案。 显然,问题出在sleep上。 通过将日志"actor/kernel sleeping"添加到本地副本的Celluloids.rb's sleep()中进行确认。
server1.rb中,sleep的调用位于server类中,该类包含Celluloid。因此,Celluloid对sleep的实现覆盖了本地的sleep
class Server
  include Celluloid::ZMQ

  ...

  def run
    loop { async.handle_message @socket.read }
  end

  def handle_message(message)

        ...

        sleep 60
  end
end

请注意来自 server1.rb 的日志 actor sleeping。日志已添加到 Celluloids.rb 的 sleep()

这将仅挂起 Celluloid 中的当前“actor”,即仅处理 client1 的当前“Celluloid 线程”睡眠。


server2.rb中,

sleep的调用位于一个名为DisplayMessage的不包含Celluloid的不同类中。

因此,这是本地的sleep

class DisplayMessage
    def self.message(message)

           ...

           sleep 60
    end
end

请注意,server2.rb 中没有任何 actor sleeping 日志。 这会暂停当前的 Ruby 任务,即 Ruby 服务器休眠(不仅仅是单个 Celluloid actor)。

修复方法?

server2.rb中必须明确指定适当的sleep时间。

class DisplayMessage
    def self.message(message)
        puts "Received at #{Time.now.strftime('%I:%M:%S %p')} and message is #{message}"
        ## Intentionally added sleep to test whether Celluloid block the main process for 60 seconds or not.
        if message == 'client-1'
           puts 'Going to sleep now'.red

           # "sleep 60" will invoke the native sleep.
           # Use Celluloid.sleep to support concurrent execution
           Celluloid.sleep 60
        end
    end
end

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接