如果我们在C#中有以下代码:
int a = 0;
int b = 0;
void A() // runs in thread A
{
a = 1;
Thread.MemoryBarrier();
Console.WriteLine(b);
}
void B() // runs in thread B
{
b = 1;
Thread.MemoryBarrier();
Console.WriteLine(a);
}
MemoryBarriers
确保写入指令在读取之前发生。但是,是否可以保证在另一个线程上读取一个线程的写入?换句话说,是否保证至少有一个线程打印1
或两个线程都可以打印0
?
我知道已经存在几个与" fresh"相关的问题。和MemoryBarrier
在C#中,例如this和this。但是,它们中的大多数都处理写入释放和读取 - 获取模式。在这个问题中发布的代码非常具体,关于是否保证在指令保持有序的情况下通过读取来查看写入。
答案 0 :(得分:3)
无法保证两个线程都会写grantYear grantAmount name
2009 3000 besancon
2010 1000 besancon
2011 0 besancon
。 It only guarantees the order of read/write operations基于此规则:
执行当前线程的处理器无法重新排序指令,以便在内存访问跟随调用后,内存访问调用之前 <{strong>>执行
1
>到MemoryBarrier
。
所以这基本上意味着MemoryBarrier
的主题不会在障碍调用之前使用变量thread A
的值 。但是如果您的代码是这样的话,它仍会缓存该值:
b
并行执行的竞争条件错误很难重现,因此我无法为您提供一定能完成上述方案的代码,但我建议您使用volatile
keyword对于不同线程使用的变量,因为它完全按照您的需要工作 - 为您提供变量的新读取:
void A() // runs in thread A
{
a = 1;
Thread.MemoryBarrier();
// b may be cached here
// some work here
// b is changed by other thread
// old value of b is being written
Console.WriteLine(b);
}
答案 1 :(得分:3)
这取决于你所说的“新鲜”。 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编译和执行后实际上是多部分指令。我试图在某种程度上说明这一点,但实际上Console.WriteLine
的调用仍然比显示的复杂得多,因此阅读a
或b
与他们的第一次之间的时间相对而言,使用可能是重要的。因为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
可能会有什么意义呢。因为连续读取产生了获取栅栏语义,所以它确保后续读取产生比先前读取更新的值。请考虑以下代码。
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
将保证在其后显示的读取将最新值 relative 返回到屏障。但是,当您实际做出决定或使用该信息时,它可能不再是最新的价值。volatile
保证读取将返回 newer 的值,而不是之前读取的同一个变量。它在任何时候都不能保证价值是最新的。答案 2 :(得分:1)
以上答案基本上是正确的。但是,为您的问题提供更简洁的解释 - “是否保证至少有一个线程打印1?” - 是的,这对记忆障碍保证了这一点。
考虑下面的表示,其中---
表示内存屏障。指令可以向后或向前移动,但它们可能不会越过障碍。
如果在同一时间调用A
和B
方法,则可以获得两个1:
| Thread A | Thread B |
| | |
| a = 1 | b = 1 |
| ------------ | ------------ |
| read b | read a |
| | |
然而,在可能性中,它们将被分开,给出0和1:
| Thread A | Thread B |
| | |
| a = 1 | |
| ------------ | |
| read b | |
| | |
| | b = 1 |
| | ------------ |
| | read a |
内存重新排序可能会导致对一个变量的读取和/或写入操作彼此移位,再次导致两个1:
| Thread A | Thread B |
| | |
| a = 1 | |
| ------------ | |
| | b = 1 |
| | |
| read b | |
| | ------------ |
| | read a |
但是,你无法将两个变量的读取和/或写入相互转移,因为障碍禁止这样做。因此,不可能得到两个0。
采用上面的第二个示例,其中b
被读为0.当在线程A上读取b
时,a
已经被写为1并且可见到其他线程,因为线程A上的内存障碍。但是,在线程B上还没有读取或缓存a
,因为线程B上的内存屏障尚未到达,因为{{1仍然是0.