使用纤程进行事件迭代?

3
作为我计算机科学教育的一部分,我正在构建一个多人Android Yatzy游戏,通过基于Ruby / EventMachine的TCP服务器将客户端连接在一起,并传递JSON消息来表示游戏事件。
然而,我对如何最优地处理游戏回合管理感到不确定。
一个典型的Yatzy游戏包括15个回合。我的实现将处理最多4个玩家。像掷骰子、保持骰子和选择得分这样的事件被发送到Ruby服务器并广播给其他玩家。
目前,我的服务器正在处理游戏回合。每次它从客户端接收到新的得分选择时,它会将得分广播给其他客户端。随后,它会广播下一个掷骰子的用户ID的消息。
我希望我的系统能够处理玩家退出而不会对其余玩家产生灰暗的副作用。我想出了一个解决方案,但我不确定是否理想。
@turnfiber = Fiber.new do
  15.times do
    @players.each do |key, value|
      Fiber.yield value
    end
  end
end
@turnfiber 是属于游戏对象的实例变量,用于表示正在进行的游戏。 @players 是散列,以玩家的唯一ID作为键,对应的玩家对象作为值。
每次轮换结束(通过得分选择提交)时都会调用 @turnfiber.resume,以获取下一个掷骰子的玩家并广播他的掷骰子权限。思想是如果玩家在第4轮离开游戏,他的客户端将发送退出消息,该消息将从@players散列中删除正在离开的玩家,并广播他的离开,因为玩家不再驻留在@players散列中,所以防止后续迭代将骰子控制权交给“死”玩家,从而避免死锁。到目前为止,我的Android客户端还不完整,因此我尚未测试这个理论在实践中是否有效。
我选择使用Fiber类的原因是希望能够对@players进行15次迭代,并依次让它们掷骰子。由于Fiber在每次调用yield时暂停循环并返回玩家,因此这是可能的。
我想了解您对这种方法的想法,特别是它的弱点以及您认为应该考虑哪些替代方案来解决这个轮换管理问题。

1
看起来纤程在恢复之间没有保留任何状态 - 实际上,它看起来像是您故意在纤程的生命周期内进行了突变(@players变量)。鉴于纤程相对于简单的内联迭代器唯一的真正优势是堆栈上下文的持久性,在这里为什么要使用纤程呢?无论如何,您仍然需要在使用纤程的代码中阻塞(或移交给EventMachine的反应堆)其产生的值。我相信这种方法会很好地工作,但我认为它不会使此应用程序的代码更简单。或者也许我漏掉了什么? - Catnapper
思考一下,我想要做的是进行嵌套迭代(15轮,2-4个玩家),并有可能突变玩家变量,最重要的是:不要一次性完成迭代,而是逐个进行。我考虑使用一个循环计数器来作为外部循环,但我仍然需要一种方法来逐个迭代玩家哈希表。在我之前使用过的Java中,可以在集合上检索迭代器对象,并调用next()来检索下一个对象。Ruby中是否有类似的东西? - Niels B.
我回复了你的评论和原帖。我喜欢这个问题,协程是一个很有趣的讨论话题,也经常被误解。 - Catnapper
1个回答

1
Ruby有枚举器,它们是协程的一种有限形式。它们的工作方式如下:
infinite_set = Enumerator.new do |yielder|
  i = 0
  loop do
    yielder.yield(i += 1)
  end
end

puts infinite_set.next
puts infinite_set.next
puts infinite_set.next

# Output:
# 1
# 2
# 3

枚举器允许外部迭代、列表的惰性求值以及函数的多个入口/出口。如果你深入了解,你会发现Ruby使用纤程(fibers)来实现它们。

我对你原始代码的理解是你想要这样的东西:

(1..15).each do |round|
  # round code
  players.each do |player|
    next unless player.active?
    # do network IO with the player object
    # if the player times out or drops, change the player active state
  end
end

我认为除非您的服务器同时运行多个游戏或需要某种进程内后台排队,否则添加另一根光纤会增加不必要的复杂性。在这种情况下,纤维和Eventmachine将非常有用。


谢谢,我理解了你的枚举器示例,但是第二个示例我不太确定。在我看来,它似乎是一次性执行并且只是中断掉已经退出游戏的玩家的迭代。所讨论的服务器确实应该具有同时托管多个游戏的能力,这就是为什么我使用EventMachine来避免阻塞代码和线程的原因。 - Niels B.
我现在已经将你的代码包装在一个枚举器中了(也许你是在暗示这个),并在每次迭代结束时调用了 Yielder.yield。看起来它也能正常工作。 - Niels B.
还有一个问题,根据ruby-doc.org,枚举器将引发必须被拯救的StopIteration异常。从RubyDocs中我所能看到的,似乎没有Fiber的alive?等价物,你能确认一下吗?唯一处理这个问题的方法是通过rescue StopIteration或者预先知道调用next的次数吗? - Niels B.
是的,整个结构需要包装在一个纤程中以实现并发,这样纤程的范围就包括每个游戏的回合计数器。我猜对于每个玩家,在网络IO上阻塞纤程是可以的(也就是说,在玩家的回合期间,每个游戏不会后台处理任何东西)。 - Catnapper
这就是使用枚举器的缺点,唯一检测其结束的方法是捕获StopIteration异常。因此,在迭代有限列表时,很少看到它们被使用,因为当迭代每个元素时,数组或集合更加适用。它们的潜力在于惰性求值。 - Catnapper

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