【问题标题】:How to pass a complex object between render-thread & updater thread?如何在渲染线程和更新线程之间传递复杂对象?
【发布时间】:2016-05-01 10:04:58
【问题描述】:

我有一个复杂的对象,我们称之为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...
}

这是我的问题:

  • 这是否正确地将新世界对象传递回渲染线程?我很确定对新克隆世界的引用是正确的,但是那个克隆世界的内容是否也与世界对象同步?
  • 应该使用“volatile”以及如何使用?
  • 例如world.getPlayer(index).getName() 是否正确同步?

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

【问题讨论】:

  • 代码不足:runInRenderThread() 是什么?语法有点奇怪——看起来像是方法调用和匿名类实例化的交叉。渲染器线程如何工作?它是某种基于事件的无限循环还是什么?它是直接与成员字段一起使用,还是也使用synchronized 块复制引用?
  • 它是 OpenGL 但它的工作方式与SwingUtilities.invokeLater(...) 相同。基本上回传是在渲染线程中调用的。我更新了这个例子。虽然它不是 Swing,但它的工作方式相同。
  • 如果你要对整个 World 进行深度复制,我建议让 World(以及其中包含的所有内容)不可变。这为您省去了很多关于同步的麻烦,您只需担心安全地发布(并获取)您的 World 引用。
  • 我要做的另一件事是将所有有关将World 移交给它自己的单独类的逻辑,这将使代码更具可读性和可测试性。
  • SwingUtilities.invokeLater() 接受 Runnable 作为其参数。你之前的东西没有参数,但是在参数之后有一个奇怪的{}部分(而不是通常的逗号),这让我感到困惑。

标签: java synchronized volatile


【解决方案1】:

我刚刚上了一门关于 Spring 框架的课程。讲师竭尽全力向我们证明,开发多线程应用程序是极其复杂的。即使您认为您将每段敏感代码都包装在同步块中,您也可能会因为运行时优化(如分支预测等)而出现意外行为

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

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

【讨论】:

    【解决方案2】:

    由于代码不清楚,因此需要进一步澄清才能回答您的第一个问题。但是,对于您的第二个问题,我可以说您可以在这里通过三种方式做到这一点:

    1. 使用synchronized,就像你在这里所做的那样(除了你没有向我们展示你的渲染器代码,所以我不能确定)。

    2. 使用volatile

    3. 使用AtomicReference

    这些都可以,但您需要小心并正确完成所有操作。您似乎非常小心不要直接修改世界,而是在发布参考之前获取它的副本并在本地修改它。这是好事。但是你做了一些奇怪的事情。我希望看到这样的东西:

    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 分配或调用设置/获取原子引用。

    现在,我们正在讨论引用本身的写入/读取。 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 变量直接传递给另一个线程,使用一个闭包,比如传递给SwingUtilities.invokeLater() 的匿名Runnable,会发生什么?这取决于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() 的调用发生在代码执行之前,那么你是安全的。否则,最好执行上述操作,尽管他们可能只是忘记在文档中提及细节。此类机制旨在将事物从一个线程传递到另一个线程,因此它们几乎总是强制执行前发生关系(通常使用某种并发队列)。

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

    【讨论】:

    • 我想我明白你的意思了。但是,我假设您的“同步”部分会将其放入Renderer.this.newWorld,然后渲染线程将再次使用Renderer.this.world 替换它? AtomicReference 也会确保世界的内容,例如world.getPlayer(index).getName() 是最新的吗?这不仅仅是参考本身吗?
    • 我不确定你用渲染线程将Renderer.this.world 替换为Renderer.this.world 是什么意思。我在这里谈论两个操作:读取和写入。更新线程读取引用,生成新版本,然后写入。渲染线程只是在需要时读取引用。是的,volatile(Java 5 及更高版本)或AtomicReference 将提供世界的最新副本,只要在参考发布后没有人修改它。我将编辑我的答案以澄清这些观点。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-07-25
    • 1970-01-01
    • 1970-01-01
    • 2021-11-24
    相关资源
    最近更新 更多