网络化摇摆游戏中的多线程:使用invokeLater vs locks

时间:2011-08-17 12:16:52

标签: java multithreading swing networking

我正在编写一个简单的自上而下的太空游戏,并且正在扩展它以允许通过具有多个玩家的网络进行游戏。我已经做了很多阅读,但这是我第一次这样做,我很欣赏一些关于选择合理设计的建议。

我的GUI是使用Swing编写的。每秒30次,计时器触发,并根据内存中游戏世界对象中的数据重新绘制我的GUI(基本上是船只和带有位置的射弹等列表)。游戏世界的物理更新也使用此计时器进行。因此,对于单人游戏实施,所有事情都发生在EDT上,这样就可以了。

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

说明我的意思:

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

VS

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

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

谢谢,

杰里米

4 个答案:

答案 0 :(得分:3)

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

接下来,从我的观点来看,invokeLater是一种相当优选的方式:如果你可以解决多线程的问题 - 你应该使用这种方式,因为多线程很难并且有很多可能的错误。

但是通过invokeLater()应用更改会给EDT带来额外的压力,因此,如果变化带来高速率,您可以观察到GUI降级。另外,如果gameWorld.updateWithNewGameInfo(数据)是havy方法,需要花费可观察的时间来完成,它可以让你甚至冻结GUI。此外,invokeLater将您的任务放在事件队列的尾部,因此它将在当前队列中的所有事件之后完成。它可能 - 在某些情况下 - 导致应用更改的延迟,这可能会使您的游戏不那么用户友好。它可能是,也可能不是你的情况,但你应该记住它

至于一般规则 - 不要将EDT用于任何耗时的任务。据我所知,到目前为止,网络数据包解析已经在您的应用程序中的单独线程中。如果耗时,应用更改可以(也应该)在单独的线程中完成。

答案 1 :(得分:1)

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

此外,您的整个游戏似乎都在EDT上运行。这是不好的做法。您应该使用model-view-controller模式拆分代码。我知道你的游戏很小,可以在EDT中运行,但你可能不应该养成这个习惯。

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

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

答案 2 :(得分:1)

方法1的优点:

  • 最小化复杂性
  • 稳定性

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

方法2的优点:

  • 可伸缩性
  • 表现

最大限度地减少在EDT线程上完成的处理,确保游戏界面和显示将保持对用户的响应。方法1现在可能有效,但如果EDT线程忙于进行非ui处理,则后续版本的游戏将无法扩展到更高级的界面。

答案 3 :(得分:0)

我的建议如下

  • 将来自不同用户(线程)的所有已加载数据推送到队列
  • 使用另一个线程从该队列中读取并从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);
            }
        });
    }
}