如果我通过共享的ByteBuffer或类似工具来处理与其他进程交互的Java进程,那么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)易失性)。
如何以最便宜的方式在Java中实现相同的排序约束。是否有什么比写一个易变的东西更少侵入?
答案 0 :(得分:4)
您可以使用lazySet,它可以比设置volatile字段快10倍,因为它不会使CPU管道停顿。例如如果需要,可以使用AtomicLong lazySet或不安全的等效项。
答案 1 :(得分:4)
sun.misc.Unsafe.putOrdered应该做你想做的事 - 一个带有锁定的商店,在x86上由volatile表示。我相信编译器不会在它周围移动指令。
这与AtomicInteger和朋友的lazySet相同,但不能直接与ByteBuffer一起使用。
与volatile
或AtomicThings
类不同,该方法适用于您使用它的特定写入,而不适用于成员的定义,因此使用它并不意味着读取任何内容。 / p>
看起来你正在尝试实现seqlock之类的东西 - 意味着你需要避免在版本计数器的读取A
之间重新排序,以及数据本身的读/写。普通的int不会削减它 - 因为JIT可能会做各种顽皮的事情。我的建议是为你的计数器使用volatile int,然后用putOrdered
将其写入。这样,你不需要为易失性写入(通常是十几个或更多周期)付出代价,同时获得易失性读取隐含的编译器障碍(这些读取的硬件障碍是无操作,使它们快速)。
所有这一切,我认为你在这里是一个灰色区域,因为lazySet
不是正式记忆模型的一部分,并且不能完全适应之前发生的事情,所以你需要更深入地了解实际的JIT和硬件实现,看看你是否可以用这种方式组合。
最后,即使使用易失性读取和写入(忽略lazySet
),我也不认为你的seqlock从java内存模型的角度来看是合理的,因为易失性写入只会在之前发生写入并稍后读取另一个线程,以及写入线程中的早期操作,但不会在写入线程上写入之后的读取和操作之间读取。换句话说,它是单向栅栏,而不是双向栅栏。我相信读取线程可以看到N + 1版本到共享区域的写入,即使它读取A == N两次。
评论澄清:
易失性只会形成单向障碍。它与某些API中WinTel使用的获取/释放语义非常相似。例如,假设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上的易失性写入”。
有趣的是,我认为你可能仅就这一点提出一个新问题 - 忘记更快的障碍 - 即使是在波动的情况下这是否仍然有用?