4

如果我有一个 Java 进程通过共享的 ByteBuffer 或类似的方式与其他进程交互,那么 C/C++ 中编译器屏障的侵入性最小的等价物是什么?不需要可移植性——我对 x86 特别感兴趣。

例如,根据伪代码,我有 2 个进程读取和写入内存区域:

p1:
    i = 0
    while true:
      A = 0
      //Write to B
      A = ++i

p2:
    a1 = A
    //Read from B
    a2 = A

    if a1 == a2 and a1 != 0:
      //Read was valid

由于 x86 上严格的内存排序(加载到单独的位置不重新排序,读取到单独的位置不重新排序)这在 C++ 中不需要任何内存屏障,只需要每次写入之间和每次读取之间的编译屏障(即 asm volatile)。

如何以最便宜的方式在 Java 中实现相同的排序约束。有什么比写到易失性更不侵入性的吗?

4

2 回答 2

4

您可以使用lazySet,它可以比设置易失性字段快10 倍,因为它不会停止CPU 管道。例如 AtomicLong lazySet 或者如果需要,您可以使用 Unsafe 等效项。

于 2013-02-02T09:47:19.830 回答
4

sun.misc.Unsafe.putOrdered 应该做你想做的事 - 一个带有 volatile 隐含在 x86 上的锁的存储。我相信编译器不会在它周围移动指令。

这与 AtomicInteger 和朋友上的lazySet 相同,但不能直接与 ByteBuffer 一起使用。

volatile类不同AtomicThings,该方法适用于您使用它的特定写入,而不是成员的定义,因此使用它并不意味着读取任何内容。

看起来您正在尝试实现类似seqlockA的东西 - 这意味着您需要避免在版本计数器的读取和数据本身的读取/写入之间重新排序。一个普通的 int 不会削减它 - 因为 JIT 可能会做各种顽皮的事情。我的建议是为您的计数器使用 volatile int,然后使用putOrdered. 这样,您无需为易失性写入付出代价(通常是十几个周期或更多),同时获得易失性读取隐含的编译器屏障(并且这些读取的硬件屏障是无操作的,使它们快速)。

说了这么多,我认为你在这里处于灰色地带,因为lazySet它不是正式记忆模型的一部分,也不完全适合发生前的推理,所以你需要更深入地了解实际的 JIT 和硬件实现,看看您是否可以以这种方式组合事物。

最后,即使使用 volatile 读取和写入(忽略lazySet),从 java 内存模型的角度来看,我认为您的 seqlock 并不合理,因为 volatile 写入仅在该写入和稍后在另一个线程上读取之间设置了一个happens-before , 和写入线程中的早期操作,但不在写入线程上的读取和写入之后的操作之间。换句话说,它是单向栅栏,而不是双向栅栏。我相信即使读取 A == N 两次,读取线程也可以看到以版本 N+1 写入您的共享区域。

评论中的澄清:

Volatile 只设置了单向障碍。它与 WinTel 在某些 API 中使用的获取/释放语义非常相似。例如,假设 A、Bv 和 C 最初都为零:

Thread 1:
A = 1;   // 1
Bv = 1;  // 2
C = 1;   // 3

Thread 2:

int c = C;  // 4
int b = Bv; // 5
int a = A;  // 6

在这里,只有 Bv 是易失的。这两个线程在概念上与您的 seqlock 编写器和读取器执行类似的操作 - 线程 1 按一个顺序写入一些内容,线程 2 以相反的顺序读取相同的内容,并尝试从中推理排序。

如果线程二有 b == 1,那么 a == 1 总是,因为 1 发生在 2 之前(程序顺序),5 发生在 6 之前(程序顺序),最关键的是 2 发生在 5 之前,因为 5 读取写入的值在 2。因此,Bv 的写入和读取就像栅栏一样。上面 (2) 的东西不能“移到下面” (2),下面的东西 (5) 不能“移到上面” 5. 请注意,我只将每个线程直接限制在一个中的移动,但是,不能同时限制两个,这将我们带到下一个例子:

与上述等效,您可能会假设如果 a == 0,则 c == 0 也是,因为 C 在 a 之后写入,并且在之前读取。但是,挥发物不能保证这一点。特别是,上述发生之前的推理并不能阻止 (3) 被移到 (2) 之上,正如线程 2 所观察到的那样,它们也不能阻止 (4) 被推到 (5) 之下。

更新:

让我们具体看一下您的示例。

我相信可能发生的是,展开 p1 中发生的写循环。

p1:

i = 0
A = 0
// (p1-1) write data1 to B
A = ++i;  // (p1-2) 1 assigned to A

A=0  // (p1-3)
// (p1-4) write data2 to B
A = ++i;  // (p1-5) 2 assigned to A

p2:

a1 = A // (p2-1)
//Read from B // (p2-2)
a2 = A // (p2-3)

if a1 == a2 and a1 != 0:

假设 p2 看到 a1 和 a2 为 1。这意味着在 p2-1 和 p1-2(以及扩展为 p1-1)之间,以及 p2-3 和 p1-2 之间之前发生过。但是,在 p2 和 p1-4 中的任何内容之间都有发生之前。所以事实上,我相信在 p2-2 对 B 的读取可以观察到在 p1-4 的第二次(可能部分完成)读取,这可以“移动到”p1-2 和 p1-3 的易失性写入之上。

有趣的是,我认为您可能仅凭这一点就提出一个新问题-忘记更快的障碍-即使使用易失性也可以吗?

于 2013-02-02T09:47:30.757 回答