【问题标题】:Multithreaded Java Program for Conway's game of life - contention at the border cells康威人生游戏的多线程 Java 程序 - 边界单元的争用
【发布时间】:2011-01-20 16:50:31
【问题描述】:

我正在学习 java 并发编程,并为 Game of Life 编写模拟。

这是我的想法:

  • 使用 int[][] 存储单元格的状态
  • 将 int[][] 划分为 t 个段并使用 t 个工作线程
  • t 线程将从其段中读取,为其段中的所有单元计算新值并更新单元。
  • 完成计算后,他们会在栅栏旁等待其他工人完成计算
  • 当越过障碍时,主线程将更新 UI。
  • 工人继续计算下一个状态。

现在将在段的公共边界处发生争用。如果一个线程在其邻居读取之前的值之前覆盖了边界单元的状态,则邻居的计算将是错误的。

我有什么选择?

  • 使用 callable 而不是 runnable 并让工作线程返回新值(而不是更新段本身)。主线程可以在越界后更新矩阵。此选项涉及将工作线程返回的结果复制到矩阵中。
  • 使用两个屏障。工作线程从其邻居的段复制边界单元并在第一个屏障处等待。一旦通过了这个障碍,他们就会继续计算下一个状态并更新适当的段。然后他们在第二道屏障等待。主线程更新 UI。

我的问题是,有没有其他方法可以处理边界单元格的争用不涉及复制数据或更有效以上两个选项?可能是在使用 ReaderWriterLock、volatile 变量或其他同步机制?

更新:到目前为止,double buffering solution by Peter 是最干净的。但我有一个问题。 由于这两个数组是共享数据,而且我们没有使用任何同步(同步访问或 volatile 变量),会不会造成可见性问题?多个 CPU 是否可以缓存数组值并在每次迭代时只更新数组的一部分?然后线程将获得边界单元格的陈旧值。这可能吗?如果不是,为什么。如果是,我该如何解决?好像是declaring two arrays volatile will not make their individual elements volatile

【问题讨论】:

  • 需要考虑的是使用 AtomicInt 而不是常规 int
  • 优势?这不会是过度同步吗?
  • 为什么要使用int,用布尔值存储不是更合逻辑更高效吗?
  • 我没有。在我当前的代码中,我使用的是 Enum[][].

标签: java multithreading synchronization conways-game-of-life java-memory-model


【解决方案1】:

我建议有 2 个 int[][] 数组。我们称它们为 A 和 B。A 将保存所有奇数“刻度”的值,B 将保存偶数刻度。

将 A 初始化为您的初始状态。然后让你的线程松开以计算每个单元格的下一个状态,并将结果放在 B 中的相应单元格中。 完成所有线程后,您将在 B 中获得新状态。现在,使用 B 计算每个单元格的下一个状态,并将结果存储在 A 中。在任何给定时间,一个数组将是只读的,而另一个数组将是只读的只写,所以永远不会有任何争用。

优点:

  • 与您现在所做的相比,无需复制数据。每个单元格只发生一次写入。
  • 不必担心边缘/角落的情况,因为算法很简单。
  • 没有正在进行的内存分配。只需在开始时分配两个数组即可。

缺点:

  • 您需要分配两倍的内存。

【讨论】:

  • 这听起来不错。但我对共享数据的可见性有疑问。查看更新。
【解决方案2】:

它不能回答您的实际问题,但我的建议是您的第一个选择......返回新值而不是让工作线程更新它们。我会更进一步,让“聚合器”将工作线程的结果组合到一个新的板状态数组中,然后丢弃第一个。我的理由是它将提高逻辑的整体可读性,因为您几乎不需要担心“全局交互”。

话虽如此,我还是有点偏见,因为我更喜欢以函数式编程方式,除非有充分的理由不这样做。

【讨论】:

    【解决方案3】:

    我会尝试以下方法:

    • 让工作人员执行计算,但只将值写回内部单元格。
    • 对于边框单元格,存储结果。
    • 计算完成后,在障碍处等待。
    • 当所有工作人员都位于第一个屏障时,然后释放并允许每个工作人员写入边界单元格。
    • 在 UI 更新时等待第二个障碍

    n x m 磁贴所需的存储空间为2 * (n + m - 1),因此通常较大的磁贴(8x8 或更大)需要相应较少的内存用于缓存值。

    【讨论】:

      【解决方案4】:

      我偶然发现了java.util.concurrent.Exchanger<V>。它充当交换点。我可能可以使用它在相邻线程之间交换单元状态列表。这比屏障要好,因为我只需要在相邻的工作人员之间进行同步。

      【讨论】:

        【解决方案5】:

        要回答您关于双缓冲缓存问题的更新,这不是问题。 CPU 具有一致的缓存,它们知道数据何时在另一个 CPU 缓存中发生更改。

        【讨论】:

        • 好的。但是由于 Java 内存模型,可见性仍然是一个问题。正如更新中的链接所示,声明数组引用 volatile 并覆盖引用将解决这个问题。虽然这也意味着,CPU 不能缓存这些值(或者在修改易失性引用后丢弃)。
        • 阅读该链接,您只需要数组引用是易失的,而不是数组的数据。您可以只拥有一个 FrontBuffer 和一个 BackBuffer 引用,并在每次迭代时交换它们。这样,缓冲区的内容仍然可以通过 cpu 缓存,这些项目不是易失性的,因此您可以很好地使用 cpu 寄存器,并且处理旧数据不会有任何问题。
        • 实际上,通过在每次使用时从内存中查找内存地址,而不是将其保存在 cpu 寄存器中,处理易失性数组引用可能会减慢您的速度。也许在每次迭代开始时,您可以将易失性引用复制到临时的非易失性引用中,因为无论如何您都不会在迭代期间更改它们。
        猜你喜欢
        • 1970-01-01
        • 2012-01-19
        • 1970-01-01
        • 1970-01-01
        • 2011-03-19
        • 1970-01-01
        • 2018-01-21
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多