为了账号安全,请及时绑定邮箱和手机立即绑定

VarHandle 获取/设置不透明

VarHandle 获取/设置不透明

慕容森 2022-12-21 13:07:14
我一直在努力了解什么VarHandle::setOpaque,真正VarHandle::getOpaque在做什么。到目前为止,这并不容易——我想我得到了一些东西(但不会在问题本身中提出它们,不要混淆水域),但总的来说,这对我来说充其量是错误的。文档:返回变量的值,按程序顺序访问...那么在我的理解中,如果我有:int xx = x; // read xint yy = y; // read y这些读数可以重新排序。另一方面,如果我有:// simplified code, does not compile, but reads happen on the same "this" for exampleint xx = VarHandle_X.getOpaque(x); int yy = VarHandle_Y.getOpaque(y);这次不能重新订货了?这就是“程序顺序”的意思?我们是在谈论在这里插入障碍以禁止这种重新排序吗?如果是这样,因为这是两个负载,是否可以实现相同的目标?通过: int xx = x; VarHandle.loadLoadFence() int yy = y;但它变得更加棘手:...但不能保证相对于其他线程的内存排序效果。我无法举出一个例子来假装我理解这部分。在我看来,这份文档是针对那些确切地知道他们在做什么的人(我绝对不是)......那么有人可以在这里阐明一下吗?
查看完整描述

3 回答

?
一只名叫tom的猫

TA贡献1906条经验 获得超2个赞

那么在我的理解中,如果我有:

int xx = x; // read x
int yy = y; // read y

这些读数可以重新排序。

这些读取可能不仅碰巧被重新排序,它们可能根本不会发生。该线程可能使用旧的、以前读取的值x和/或y它之前写入这些变量的值,而实际上,写入可能尚未执行,因此“读取线程”可能使用值,而不是其他线程当时可能知道并且不在堆内存中(并且可能永远不会)。

另一方面,如果我有:

// simplified code, does not compile, but reads happen on the same "this" for example
int xx = VarHandle_X.getOpaque(x); 
int yy = VarHandle_Y.getOpaque(y);

这次不能重新订货了?这就是“程序顺序”的意思?

简单地说,不透明读写的主要特征是,它们会实际发生。这意味着它们不能相对于至少具有相同强度的其他内存访问重新排序,但这对普通读写没有影响。

术语程序顺序由 JLS 定义:

… t的程序顺序是一个总顺序,它反映了根据t的线程内语义执行这些操作的顺序。

这是为表达式和语句指定的评估顺序。我们感知效果的顺序,只要只涉及一个线程。

我们是在谈论在这里插入障碍以禁止这种重新排序吗?

不,不涉及障碍,这可能是短语“ ......但不能保证相对于其他线程的内存排序效果”背后的意图。

或许,我们可以说不透明访问的工作方式有点像volatileJava 5 之前的版本,强制读取访问以查看最近的堆内存值(这只有在写入端也使用不透明或更强大的模式时才有意义),但没有对其他读取或写入的影响。

那么你能用它做什么呢?

一个典型的用例是一个取消或中断标志,它不应该建立一个happens-before关系。通常,已停止的后台任务没有兴趣感知停止任务在发出信号之前所做的操作,而只会结束自己的活动。因此,使用不透明模式写入和读取标志足以确保最终注意到信号(与正常访问模式不同),但不会对性能产生任何额外的负面影响。

同样,后台任务可以写入进度更新,如百分比数字,报告 (UI) 线程应该及时注意到,而在最终结果发布之前不需要happens-before关系。

如果您只想对 和 进行原子访问,而没有任何其他影响,它也很有longdouble

由于使用字段的真正不可变对象final不受数据竞争的影响,因此您可以使用不透明模式及时发布不可变对象,而不会产生发布/获取模式发布的更广泛影响。

一种特殊情况是定期检查预期值更新的状态,一旦可用,就以更强的模式查询该值(或显式执行匹配的围栏指令)。原则上,无论如何只能在写入和后续读取之间建立happens-before关系,但由于优化器通常没有识别此类线程间用例的视野,因此性能关键代码可以使用不透明访问来优化这样的场景。


查看完整回答
反对 回复 2022-12-21
?
人到中年有点甜

TA贡献1895条经验 获得超7个赞

不透明意味着执行不透明操作的线程保证按照程序顺序观察自己的动作,仅此而已。

其他线程可以自由地以任何顺序观察线程的动作。在 x86 上这是一个常见的情况,因为它有

使用存储缓冲区转发命令写入

内存模型,因此即使线程在加载之前确实存储。存储可以缓存在存储缓冲区中,并且在任何其他内核上执行的某些线程以相反的顺序加载存储而不是存储加载来观察线程操作。所以不透明操作是在 x86 上免费完成的(在 x86 上我们实际上也有免费获取,有关其他一些架构及其内存模型的详细信息,请参阅这个极其详尽的答案:https ://stackoverflow.com/a/55741922/8990329 )

为什么有用?好吧,我可以推测,如果某个线程观察到一个存储在不透明内存语义中的值,那么后续读取将观察到“至少这个或以后”的值(普通内存访问不提供这样的保证,是吗?)。

此外,由于 Java 9 VarHandles 在某种程度上与 CI 中的获取/释放/消费语义相关,因此值得注意的是,不透明访问类似于memory_order_relaxed标准中定义的内容,如下所示:

对于memory_order_relaxed,没有操作命令内存。


查看完整回答
反对 回复 2022-12-21
?
白衣染霜花

TA贡献1796条经验 获得超10个赞

我自己一直在与不透明作斗争,文档当然不容易理解。

从上面的链接:

不透明操作是按位原子和连贯有序的。

按位原子部分是显而易见的。一致排序意味着加载/存储到单个地址有一些总顺序,每个到达看到它之前的最近地址并且顺序与程序顺序一致。有关一些连贯性示例,请参阅以下JCStress 测试。

Coherence 不在不同地址的加载/存储之间提供任何排序保证,因此它不需要提供任何栅栏以便对不同地址的加载/存储进行排序。

使用不透明,编译器将发出它看到的加载/存储。但是仍然允许底层硬件将加载/存储重新排序到不同的地址。

我将您的示例升级为消息传递石蕊测试:

thread1:

X.setOpaque(1);

Y.setOpaque(1);


thread2:

ry = Y.getOpaque();

rx = X.getOpaque();

if (ry == 1 && rx == 0) println("Oh shit");

在允许重新排序 2 个存储或 2 个加载(同样是 ARM 或 PowerPC)的平台上,上述操作可能会失败。不透明不需要提供因果关系。JCStress 也有一个很好的例子。


此外,以下 IRIW 示例可能会失败:


thread1:

X.setOpaque(1);


thread2:

Y.setOpaque(1);


thread3:

rx_thread3 = X.getOpaque();

[LoadLoad]

ry_thread3 = Y.getOpaque();


thread4:

ry_thread4 = Y.getOpaque();

[LoadLoad]

rx_thread4 = X.getOpaque();

难道我们最终得到 rx_thread3=1,ry_thread3=0,ry_thread4=1 而 rx_thread4 为 0 吗?


对于不透明,这可能会发生。即使负载被阻止重新排序,不透明访问也不需要多副本原子性(可以看到不同 CPU 发出的不同地址的存储以不同的顺序)。


Release/acquire 比 opaque 强,因为 release/acquire 允许失败,因此 opaque 允许失败。所以 Opaque 不需要提供共识。


查看完整回答
反对 回复 2022-12-21
  • 3 回答
  • 0 关注
  • 82 浏览

添加回答

举报

0/150
提交
取消
意见反馈 帮助中心 APP下载
官方微信