- android - 多次调用 OnPrimaryClipChangedListener
- android - 无法更新 RecyclerView 中的 TextView 字段
- android.database.CursorIndexOutOfBoundsException : Index 0 requested, 光标大小为 0
- android - 使用 AppCompat 时,我们是否需要明确指定其 UI 组件(Spinner、EditText)颜色
我正在努力思考内存屏障和 volatile 读/写的微妙之处。我正在阅读 Joseph Albahari 的线程文章:
http://www.albahari.com/threading/part4.aspx
并且在读/写之前何时需要内存屏障以及之后何时需要内存屏障的问题上绊倒了。在“full fences”部分的这段代码中,他在每次写入之后和每次读取之前放置了一个内存屏障:
class Foo
{
int _answer;
bool _complete;
void A()
{
_answer = 123;
Thread.MemoryBarrier(); // Barrier 1
_complete = true;
Thread.MemoryBarrier(); // Barrier 2
}
void B()
{
Thread.MemoryBarrier(); // Barrier 3
if (_complete)
{
Thread.MemoryBarrier(); // Barrier 4
Console.WriteLine (_answer);
}
}
}
他继续解释:
Barriers 1 and 4 prevent this example from writing “0”. Barriers 2 and 3 provide a freshness guarantee: they ensure that if B ran after A, reading _complete would evaluate to true.
问题 #1: 我对障碍 1 和 4 没有问题,因为它会阻止跨这些障碍重新排序。我不完全理解为什么障碍 2 和 3 是必要的。有人可以解释一下吗,尤其是考虑到如何在 Thread
类中实现 volatile 读取和写入(接下来解释)?
现在我真正开始感到困惑的是,这是 Thread.VolatileRead/Write()
的实际实现:
[MethodImplAttribute(MethodImplOptions.NoInlining)]
public static void VolatileWrite (ref int address, int value)
{
MemoryBarrier(); address = value;
}
[MethodImplAttribute(MethodImplOptions.NoInlining)]
public static int VolatileRead (ref int address)
{
int num = address; MemoryBarrier(); return num;
}
如您所见,与前面的示例相比,内置的 volatile 函数在每次写入之前(而不是之后)和每次读取之后(而不是之前)放置了内存屏障。因此,如果我们基于内置的 volatile 函数用等效版本重写前面的示例,它会看起来像这样:
class Foo
{
int _answer;
bool _complete;
void A()
{
Thread.MemoryBarrier(); // Barrier 1
_answer = 123;
Thread.MemoryBarrier(); // Barrier 2
_complete = true;
}
void B()
{
if (_complete)
{
Thread.MemoryBarrier(); // Barrier 3
Console.WriteLine (_answer);
Thread.MemoryBarrier(); // Barrier 4
}
}
}
问题 #2:这两个 Foo 类在功能上是否相同?为什么或者为什么不?如果需要障碍 2 和 3(在第一个 Foo 类中)来保证写入值和读取实际值,那么 Thread.VolatileXXX
方法不会有点无用吗?
StackOverflow 上有几个类似的问题,其答案已被接受,例如“障碍 2 确保对 _complete 的写入未被缓存”,但没有一个解决为什么 Thread.VolatileWrite()
将内存放入如果是这种情况,则在写入之前设置屏障,如果 Thread.VolatileRead()
将内存屏障放在读取之后但仍保证最新值,则为什么需要屏障 3。我认为这是最让我失望的地方。
更新:
好吧,经过更多的阅读和思考后,我有了一个理论,并用我认为可能相关的属性更新了源代码。我认为 Thread.VolatileRead/Write
方法中的内存屏障根本不是为了确保值的“新鲜度”,而是为了强制执行重新排序保证。在读取之后和写入之前放置屏障可确保在任何读取之前不会移动任何写入(反之亦然)。
据我所知,x86 上的所有写入都通过使其他内核上的缓存行无效来保证缓存一致性,因此只要值未缓存在寄存器中,就可以保证“新鲜度”。我关于 VolatileRead/Write
确保值不在寄存器中的方法的理论,这可能有点偏离,但我认为我在正确的轨道上,是他们依靠 .NET 实现细节,如果它们被标记为 MethodImplOptions.NoInlining
(正如您在上面看到的那样),则需要将值传递给方法/从方法传递,而不是内联为一个局部变量,因此必须从内存/缓存而不是直接通过寄存器访问,从而消除了在写入之后和读取之前对额外内存屏障的需要。我不知道情况是否如此,但这是我认为它正常工作的唯一方式。
谁能证实或否认是这种情况?
最佳答案
I don't think the memory barriers in the
Thread.VolatileRead/Write
methods are there to ensure "freshness" of the values at all, but rather to enforce the reordering guarantees.
没错。
Putting the barrier after reads and before writes ensures that no writes will be moved before any reads (but not vice versa).
完整的内存屏障同时具有获取和释放语义,它可以防止先前的内存访问被重新排序到屏障之后以及后续的内存访问被重新排序到屏障之前。
Can anyone confirm or deny that this is the case?
对于在 x86 中 Microsoft 的 .NET 实现中的写入,您可能是正确的,但同样不适用于读取。可以通过 JIT 编译器(可能不使用 no-inlining 属性)或 CPU(即使使用 no-inlining 属性)在先前的访问和内存屏障之间重新排序读取。
但是,这不应改变运行代码看到的内容,尽管读取可能看不到最新鲜值。
int value = 0;
bool done = false;
// in thread 1
value = 123;
Thread.VolatileWrite(ref done, true);
// in thread 2
Thread.SpinUntil(() => Thread.VolatileRead(ref done));
Console.WriteLine(value); // guaranteed 123 due to the memory barrier
在内存模型较弱的其他架构中,写入可以在后续内存访问后重新排序,在极端情况下,其他线程可能直到下一个内存屏障才可见。不过,对于循环,这不是什么大问题。
无论如何,我的建议是不要使用 Thread.VolatileRead
和 Thread.VolatileWrite
。
读取和写入 volatile
字段,Volatile.Read
和 Volatile.Write
方法提供正确的语义。
尽管 Volatile.Read
和 Volatile.Write
方法是在 C# 中实现的,就像 Thread.VolatileRead
和 Thread 一样。 VolatileWrite
,CLR 将方法替换为具有实际 volatile 读/写语义的 native 版本。
关于c# - volatile 读/写和 Thread.MemoryBarrier 排序,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/42744595/
这将是一篇很长的文章,为了将其上下文化并提供尽可能多的信息,我必须仔细浏览各种链接和引号——这通常是我们进入 C/C++ 标准兔子洞的唯一方法。如果您对这篇文章有更好的引用或任何其他改进,请告诉我。但
我想知道 volatile 关键字与 register、const 和 static 结合的不同用途关键词。我不确定有什么影响,所以我认为: register volatile int T=10; 建
让我们考虑以下 Java 代码 int x = 0; int who = 1 Thread #1: (1) x++; (2) who = 2; Thread #2 while(who
有一个函数“remove_cv”(http://en.cppreference.com/w/cpp/types/remove_cv)可以删除常量和 volatile 。 我的问题是为什么可以从“con
我正在尝试在下面的“MpscQueue.h”中的嵌入式目标上实现多个生产者(通过中断)、单个消费者(通过应用程序线程)队列。 我想知道我是否可以安全地删除一些 volatile下面的用法(见内联问
我的问题适用于最初为 null 的字段,然后初始化为非 null 值,然后不再更改。 由于该字段需要尽快可供所有线程使用,因此我需要使用 volatile 。 但是,如果我想尽可能避免 volatil
我以前见过几次类似 fld = fld 的东西,但在所有这些情况下,可以消除虚拟写入并获得更好的性能。 public class Tst{ public volatile int fld =
看完this question和 this (尤其是第二个答案)我对 volatile 及其与内存屏障有关的语义感到非常困惑。 在上面的例子中,我们写入了一个 volatile 变量,这会导致一个 m
如下所示,该程序有一个共享 var flag,但不带 volatile : public class T { public static void main(String[] args) {
我明白声明 int *volatile ptr; 表示指针本身是volatile int a=10; int *volatile ptr=&a; 现在 ptr 和 a 都在更新。会不会导致访问ptr时
最近我需要比较两个 uint 数组(一个是 volatile 数组,另一个是非 volatile 数组),结果令人困惑,我一定是对 volatile 数组有一些误解。 我需要从输入设备读取一个数组并将
这两个 C 定义有什么区别? volatile uint32_t *ptr1 = (volatile uint32_t *)0x20040000; volatile uint32_t *ptr1 =
// structure is like this, but not exact formation. class queue { volatile List worksWaiting; }
考虑以下这段代码: struct S{ int i; S(int); S(const volatile S&); }; struct S_bad{ int i; }; vola
在 Windows x64 上,考虑到一些额外的见解,何时允许编译器将 ABI 标记为 volatile 的寄存器视为非 volatile 寄存器?我有一个反汇编函数,其中 r11 用于在函数调用后恢
我对下面的代码段有疑问。结果可能有 [0, 1, 0] 的结果(这是用 JCStress 执行的测试)。那么这怎么会发生呢?我认为应该在写入 Actor2 (guard2 = 1) 中的 guard2
好吧,假设我有一堆变量,其中一个声明为 volatile: int a; int b; int c; volatile int v; 如果一个线程写入所有四个变量(最后写入 v),而另一个线程读取所有
我试图理解为什么这个例子是一个正确同步的程序: a - volatile Thread1: x=a Thread2: a=5 因为存在冲突访问(存在对 a 的写入和读取),所以在每个顺序一致性执行中,
我正在编写一个需要同时支持 volatile 和非 volatile 实例的类( volatile 实例使用原子操作,非 volatile 实例使用常规操作),并且想知道我是否以正确的方式进行处理。到
我正在为 Cortex-M0 CPU 和 gcc 编写代码。我有以下结构: struct { volatile unsigned flag1: 1; unsigned flag2: 1
我是一名优秀的程序员,十分优秀!