如何在render-thread&之间传递一个复杂的对象更新程序线程?

时间:2016-05-01 10:04:58

标签: java synchronized volatile

我有一个复杂的对象,我们称之为_id,其中包含其他具有玩家数据的对象,描述世界地图的对象等。它用于我回归的回合制游戏屏幕上的World,但每回合使用一个单独的线程来更新World,因为这需要几秒钟的时间才能运行。在渲染线程中运行它只会冻结屏幕。

这是World在非渲染线程中更新的方式以及传递回渲染线程的方式:

World

这就是渲染的工作原理:

// copy the world that is used for rendering
World world;
synchronized (Renderer.this.sync) {
  world = Renderer.this.world;
}
World clone = Util.clone(world);

// update the world 
Updater.update(clone);

// pass the updated world back to the render thread
SwingUtilities.invokeLater(new Runnable() {
  public void run() {
    synchronized (Renderer.this.sync) {
      Renderer.this.world = clone;
    }
  }
});

以下是我的问题:

  • 这是否正确地将新的世界对象传回给渲染线程?我很确定对新克隆世界的引用是正确的,但克隆世界的内容是否也与世界对象同步?
  • 应该"不稳定"被使用以及如何使用?
  • 例如// member variable: our sync-object public final Object sync = new Object(); // member variable for world public World world; public void render() { ... renders the world object... } 是否已正确同步?

不确定问题是否清楚?我会根据需要澄清。 - 谢谢!

2 个答案:

答案 0 :(得分:0)

我刚做了一个关于Spring框架的课程。讲师竭尽全力向我们证明开发多线程应用程序非常复杂。即使您认为在同步块中包装了每一段敏感代码,由于运行时优化(如分支预测等),您可能会遇到意外行为。

volatile关键字用于控制这种不可预测的行为(即对分支预测并行处理设置障碍)

我在答案上放了一个免责声明,我没有太多开发多线程应用程序的经验。话虽如此,我会将World对象设计为单例,这样它只有一个副本,并且一次只有一个线程可以访问它。

答案 1 :(得分:0)

需要更多说明来回答您的第一个问题,因为代码不清楚。但是,对于你的第二个问题,我可以说你可以在三个方面做到这一点:

  1. 在这里使用synchronized(除了您没有向我们展示您的渲染器代码,因此我无法确定)。

  2. 使用volatile

  3. 使用AtomicReference

  4. 任何这些都可以,但你需要小心并做好一切。您似乎非常小心不直接修改世界,而是在发布引用之前先复制它并在本地修改它。这是好事。但是你做了一些奇怪的事情。我期待看到类似的东西:

    synchronized (Renderer.this.sync) {
      world = Renderer.this.world;
    }
    World clone = Util.clone(world);
    // update the world 
    Updater.update(clone);
    // publish the reference back to the renderer
    synchronized (Renderer.this.sync) {
      Renderer.this.world = clone;
    }
    

    这一切都在非渲染器线程中完成。 然后,在发布引用后,您应该以某种方式通知渲染器,以便将发布的引用复制到局部变量(同样,在synchronized块中),然后呈现它。通过这种方式,您可以确保它获得正确更新的世界 world引用在呈现过程中永远不会热交换。

    这基本上是关于两个操作:读取两个线程之间共享的引用并编写它。有一个叫happens-before relationship的东西。适用于这种情况:

    • 监视器上的解锁发生 - 在该监视器上的每个后续锁定之前;
    • 在对该字段的每次后续读取之前发生对易失性字段(第8.3.1.4节)的写入;
    • AtomicReference.set在每个后续AtomicReference.get之前发生(根据the docs,get和set具有与volatile读/写相同的语义)。

    这就是为什么你可以使用volatile字段或AtomicReference做同样的事情(两者都会提供比synchronized更好的性能)。只需用synchronized赋值替换那些volatile赋值块,或者调用原子引用的set / get。

    现在,我们正在讨论引用本身的写入/读取。 world的字段或它引用的对象怎么样?好吧,在一个线程中发生的事情也发生在关系之前,因为它们来自程序顺序。因此,无论您写入world对象的字段(或属于其对象图的任何内容,或其他任何地方)之前您发布对克隆的引用,都将是对渲染线程可见:

    clone.setSomeProperty(someValue); // this...
    synchronized (Renderer.this.sync) {
        Renderer.this.world = clone; // ...happens-before this, which in turn...
    }
    // ...
    synchronized (Renderer.this.sync) {
        world = Renderer.this.world; // ...happens-before this in the rendering thread
        // (assuming the rendering thread entered this block after the
        //  updating thread entered its block, otherwise we'll just see
        //  the old world here and will pick up the new one on the next try)
    }
    

    如果使用传递给clone的匿名Runnable之类的闭包将SwingUtilities.invokeLater()变量直接传递给其他线程,会发生什么?这取决于SwingUtilities.invokeLater()的实施方式。不幸的是,这个主题的文档并不十分清楚。我猜它是安全的,但绝对肯定我宁愿从当前线程安全地发布新引用(如上例所示),或者,依赖于final字段语义,如下所示:

    SwingUtilities.invokeLater(new Runnable() {
      private final World world = clone; // final semantics enforced
      public void run() {
        Renderer.this.world = world; // this runs after the Runnable is constructed
        // and therefore is guaranteed to see the updated version of the world
      }
    });
    

    您应该参考有关用于在渲染器线程中执行代码的OpenGL函数的文档。如果它在代码执行之前发出了runInRenderThread()的调用,那么你就是安全的。否则,最好做上面的事情,尽管他们可能只是忘了提到文档中的细节。这些机制旨在将事物从一个线程传递到另一个线程,因此它们几乎总是强制执行before-before关系(通常使用某种并发队列)。

    最后,你不应该做的一件事就是在你以一种或另一种方式向渲染器线程发布它的引用之后修改世界。这将打破之前发生的关系,并可能导致不一致。所以没有“哦,等等,我已经发布了它,但我需要尽快更新这个重要的字段!” - 抱歉,这将不得不等到下次更新。