这取决于你所说的“新鲜”是什么意思。Thread.MemoryBarrier
将通过从其指定的内存位置加载变量来强制对其进行第一次读取。如果这就是您所说的“新鲜”的全部意思,那么答案是肯定的。无论他们是否意识到,大多数程序员都使用更严格的定义进行操作,这就是问题和混乱开始的地方。请注意,在此定义下,通过易失性读取volatile
和其他类似机制不会产生“新”读取,但会在不同定义下产生。继续阅读以了解如何。
我将使用向下箭头 ↓ 表示易失性读取,使用向上箭头 ↑ 表示易失性写入。将箭头视为推开任何其他读取和写入。只要没有指令通过向下箭头向上或向下通过向上箭头,生成这些内存栅栏的代码就可以自由移动。然而,内存栅栏(箭头)被锁定在它们最初在代码中声明的位置。Thread.MemoryBarrier
生成一个完整的屏障,因此它具有读取-获取和释放-写入语义。
int a = 0;
int b = 0;
void A() // runs in thread A
{
register = 1
a = register
↑ // Thread.MemoryBarrier
↓ // Thread.MemoryBarrier
register = b
jump Console.WriteLine
use register
return Console.WriteLine
}
void B() // runs in thread B
{
register = 1
b = register
↑ // Thread.MemoryBarrier
↓ // Thread.MemoryBarrier
register = a
jump Console.WriteLine
use register
return Console.WriteLine
}
请记住,一旦 C# 行经过 JIT 编译和执行,它们实际上就是多部分指令。我试图在某种程度上说明这一点,但实际上调用 ofConsole.WriteLine
仍然比显示的要复杂得多,因此读取a
orb
和它们第一次使用之间的时间相对而言可能很重要。因为Thread.MemoryBarrier
产生了一个获取栅栏,所以不允许读取向上浮动并超过调用。Thread.MemoryBarrier
因此,相对于调用而言,读取是“新鲜的” 。但是,相对于调用实际使用它的时间,它可能是“陈旧的” Console.WriteLine
。
现在让我们考虑一下如果我们用关键字替换Thread.MemoryBarrier
调用,您的代码会是什么样子。volatile
volatile int a = 0;
volatile int b = 0;
void A() // runs in thread A
{
register = 1
↑ // volatile write
a = register
register = b
↓ // volatile read
jump Console.WriteLine
use register
return Console.WriteLine
}
void B() // runs in thread B
{
register = 1
↑ // volatile write
b = register
register = a
↓ // volatile read
jump Console.WriteLine
use register
return Console.WriteLine
}
你能看出变化吗?如果你眨眼,那你就错过了。比较两个代码块之间的箭头(内存栅栏)的排列。在第一种情况 ( Thread.MemoryBarrier
) 中,不允许在内存屏障之前的时间点发生读取。但是,在第二种情况下(volatile
),读取可以无限期地冒泡(因为有向下箭头将它们推开)。在这种情况下,可以提出一个合理的论点,Thread.MemoryBarrier
如果将其放在阅读之前而不是解决方案,则可以产生“更新鲜”的阅读volatile
。但是,你还能声称阅读是“新鲜的”吗?不是真的,因为当它被使用时,Console.WriteLine
它可能不再是最新的值了。
那么volatile
你可能会问,使用的意义何在。因为连续读取会产生acquire-fence语义,所以它确实保证以后的读取会产生比前一次读取更新的值。考虑以下代码。
volatile int a = 0;
void A()
{
register = a;
↓ // volatile read
Console.WriteLine(register);
register = a;
↓ // volatile read
Console.WriteLine(register);
register = a;
↓ // volatile read
Console.WriteLine(register);
}
密切注意这里可能发生的事情。线条register = a
代表读取。注意↓箭头的位置。因为它是在读取之后放置的,所以没有什么可以阻止实际读取向上浮动。Console.WriteLine
它实际上可以在上一次调用之前浮动。所以在这种情况下,不能保证Console.WriteLine
使用a
. 但是,它保证使用比上次调用时更新的值。简而言之,这就是它的用处。这就是为什么您会看到许多无锁代码在 while 循环中旋转,以确保在假定预期操作成功之前,对 volatile 变量的先前读取等于当前读取。
我想总结几点。
Thread.MemoryBarrier
将保证出现在它之后的读取将返回相对于屏障的最新值。但是,当您实际做出决定或使用该信息时,它可能不再是最新值。
volatile
保证读取将返回一个比先前读取的相同变量更新的值。它绝不保证该值是最新的。
- “新鲜”的含义需要明确定义,但可能因情况而异,因开发人员而异。只要可以正式定义和表达,就没有任何意义比其他任何意义都正确。
- 这不是一个绝对的概念。您会发现将“新鲜”定义为相对于其他事物(例如内存屏障的生成或先前的指令)更有用。换句话说,“新鲜度”是一个相对概念,就像爱因斯坦狭义相对论中速度与观察者的关系一样。