【问题标题】:Multithreading in a networked swing game: using invokeLater vs locks网络摇摆游戏中的多线程:使用 invokeLater 与锁
【发布时间】:2011-10-28 21:18:29
【问题描述】:

我正在编写一个简单的自上而下的太空游戏,并且正在扩展它以允许通过网络与多个玩家一起玩。我已经阅读了相当多的内容,但这是我第一次这样做,我会很感激一些关于选择合理设计的建议。

我的 GUI 是使用 Swing 编写的。每秒 30 次,计时器触发,并根据内存中 gameWorld 对象中的数据重新绘制我的 GUI(本质上是带有位置的船只和射弹列表等)。游戏世界的物理更新也使用这个计时器进行。因此,对于单人游戏实现,一切都发生在 EDT 上,并且运行良好。

现在,我有单独的线程来处理来自其他玩家的传入数据包。我想根据这些数据包包含的内容更新我的 gameWorld 对象中的数据。我的问题是,我应该使用 invokeLater 来进行这些更改,还是应该使用锁来避免并发问题?

为了说明我的意思:

runMethodOfInputThread() {
    while(takingInput) {
        data = receiveAndInterpretIncomingPacket();  // blocks
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                gameWorld.updateWithNewGameInfo(data);
            }
        });
    }
}

runMethodOfInputThread() {
    while(takingInput) {
        data = receiveAndInterpretIncomingPacket();  // blocks
        synchronize (gameWorldLock) {
            gameWorld.updateWithNewGameInfo(data);
        }
    }
}

后者还需要在 EDT 访问游戏世界的任何地方使用类似的同步块,所以在我看来,使用 invokeLater 会更容易实现。但我认为这两种方法都行得通吗?还有其他重要的优点/缺点需要记住吗?

谢谢,

杰里米

【问题讨论】:

    标签: java multithreading swing networking


    【解决方案1】:

    嗯,首先你不需要只选择一种方法。您可以使用锁使您的数据结构“只是为了确定”线程安全(因为您的应用程序已经是多线程的),并使用 invokeLater 仅在 EDT 中实际应用更改——在这种情况下,JIT 可能会优化您的锁定, 接近于 0。

    接下来,从我的角度来看,invokeLater 是一种更可取的方式:如果你可以绕过处理多线程——你应该使用这种方式,因为多线程很难并且可能会出现很多错误。

    但是通过 invokeLater() 应用更改会给 EDT 带来额外的压力,因此,如果更改速度很快,您可以观察到 GUI 退化。此外,如果 gameWorld.updateWithNewGameInfo(data) 方法需要可观察的时间才能完成,它甚至会让你的 GUI 冻结。此外,invokeLater 将您的任务放在事件队列的尾部,因此它将在当前队列中的所有事件之后完成。在某些情况下,它可能会导致应用更改的延迟,从而降低游戏的用户友好性。这可能是,也可能不是你的情况,但你应该牢记这一点

    至于一般规则——不要将 EDT 用于任何耗时的任务。据我了解,网络数据包解析已经在您的应用程序的单独线程中。应用更改也可以(并且应该)在单独的线程中完成,如果这很耗时的话。

    【讨论】:

    • 经验法则:在 EDT 中尽可能少做,始终在 EDT 中更新组件,并尝试选择 invokeLater() 而不是 invokeAndWait()。
    【解决方案2】:

    方法 1 的优点:

    • 最小化复杂度
    • 稳定性

    通过将对“gameWorld”变量的访问限制为 EDT 线程,不需要锁定机制。并发编程很复杂,并且要求程序员在访问线程间共享的对象时对整个源库保持警惕。这是可能的 程序员在某些情况下忘记同步,导致游戏状态受损或程序失败。

    方法 2 的优点:

    • 可扩展性
    • 性能

    尽量减少在 EDT 线程上完成的处理,确保游戏界面和显示始终响应用户。方法 1 目前可能可行,但如果 EDT 线程忙于进行非 UI 处理,您的游戏的后续版本将无法扩展到更高级的界面。

    【讨论】:

    • 非常感谢您所有深思熟虑的回复,您给了我很多思考。我认为这个总结了我一直在寻找的最好的东西。我想我现在会选择选项 1。在输入线程gameWorld.updateWithNewGameInfo(data) 上完成网络数据包的解析可能不会特别繁重(它真正所做的只是在gameWorld 中设置一些变量)。如果事情开始陷入僵局,或者我决定要支持大量玩家,我会重新考虑。
    【解决方案3】:

    不是第二个。您希望尽可能少地在 EDT 中运行。如果您正在等待 EDT 中的锁,这与直接在 EDT 中运行所有其他代码(在锁的另一侧)一样糟糕,因为 EDT 必须等待其他所有操作完成。

    此外,您的整个游戏似乎都在 EDT 上运行。这是不好的做法。您应该使用模型-视图-控制器模式拆分您的代码。我了解您的游戏很小,可以在 EDT 中运行,但您可能不应该养成这个习惯。

    您应该让游戏逻辑从计时器线程 (java.util.concurrent.ScheduledThreadPoolExecutor) 运行,并在每个周期结束时将数据“发送”到 EDT 并使用 invokeLater 重新绘制。

    您还应该有一些单独的线程来读取套接字,并且该线程应该写入与您在计时器游戏线程中使用的对象共享锁的对象。

    【讨论】:

      【解决方案4】:

      我的建议如下

      • 将来自不同用户(线程)的所有加载数据推送到队列中
      • 使用另一个线程从该队列中读取并从 EDT 更新 UI

      它应该避免您的并发问题。它是如何实现的

      runMethodOfInputThread() {
          while(takingInput) {
              data = receiveAndInterpretIncomingPacket();  // blocks
              blockingQueue.add(data);
          }
      }
      
      runMethodOfUPdateUIThread() {
          while(updatingUI) {
              data = blockingQueue.take();
              SwingUtilities.invokeLater(new Runnable() {
                  public void run() {
                      gameWorld.updateWithNewGameInfo(data);
                  }
              });
          }
      }
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2014-03-06
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2011-06-26
        • 1970-01-01
        相关资源
        最近更新 更多