我什么时候可以保证一个线程上更改的值对其他线程可见?
When can I guarantee value changed on one thread is visible to other threads?
我知道线程可以缓存一个值 ignore changes made on another thread,但我想知道它的变体。线程是否可能更改缓存值,然后该值永远不会离开其缓存,因此对其他线程永远不可见?
例如,此代码是否可以打印 "Flag is true",因为线程 a
永远看不到线程 b
对 [=13 所做的更改=]? (我不能让它这样做,但我无法证明它或它的某些变体不会。)
var flag = true;
var a = new Thread(() => {
Thread.Sleep(200);
Console.WriteLine($"Flag is {flag}");
});
var b = new Thread(() => {
flag = false;
while (true) {
// Do something to avoid a memory barrier
}
});
a.Start();
b.Start();
a.Join();
我可以想象线程 b
flag
可以缓存在 CPU 寄存器中然后设置为 false
,当 b
进入 while
循环它从来没有机会(或从不关心)将 flag
的值写回内存,因此 a
总是将 flag
视为 true。
从 this answer 中列出的内存屏障生成器来看,在我看来,这在理论上是可行的。我对么?我无法在实践中证明这一点。任何人都可以想出一个例子吗?
我不完全确定这是否能回答您的问题,但我可以。
如果您 运行 以下代码的发布(非调试)版本,它将永远不会终止,因为 waitForFlag()
永远不会看到 flag
的更改版本。
但是,如果您注释掉任何指定的行,程序将终止。
看起来在 while (flag)
循环中调用外部库会导致优化器不缓存 flag
的值。
另外(当然)使 flag
易变会阻止这种优化。
using System;
using System.Threading;
using System.Threading.Tasks;
namespace Demo
{
class Program
{
void run()
{
Task.Factory.StartNew(resetFlagAfter1s);
var waiter = Task.Factory.StartNew(waitForFlag);
waiter.Wait();
Console.WriteLine("Done");
}
void resetFlagAfter1s()
{
Thread.Sleep(1000);
flag = false;
}
void waitForFlag()
{
int x = 0;
while (flag)
{
++x;
// Uncommenting this line make this thread see the changed value of flag.
// Console.WriteLine("Spinning");
}
}
// Uncommenting "volatile" makes waitForFlag() see the changed value of flag.
/*volatile*/ bool flag = true;
static void Main()
{
new Program().run();
}
}
}
研究Thread.MemoryBarrier,你就会变成金子。
按照从最容易正确到最容易搞砸的顺序排列
- reading/writing
时使用锁
- 使用
Interlocked
class 上的功能
- 使用内存屏障
Is it possible for a thread to change a cached value, which then never leaves its cache and so is never visible to other threads?
如果我们从字面上谈论硬件缓存,那么我们需要讨论特定的处理器系列。如果您在 x86(和 x64)上工作(看起来很可能),您需要知道这些处理器实际上具有比 .NET 所需的内存模型强得多的内存模型。在 x86 系统中,缓存保持一致性,因此其他处理器不会忽略任何写入。
如果我们谈论优化,其中特定内存位置已被读入处理器寄存器,然后 后续 从内存读取只是重用该寄存器,那么就没有' 写入端的类似类似物。您会注意到,在我们假设没有其他任何东西改变该内存位置并且我们可以重用该寄存器之前,总是至少有一个从实际内存位置读取。
在写入方面,我们被告知要将某些内容推送到特定的内存位置。我们必须至少推送到该位置一次,并且始终将先前已知的值存储在该位置(特别是如果我们的线程从未从中读取)在一个单独的寄存器中只是为了能够执行比较可能是一种去优化并省略写入操作。
我知道线程可以缓存一个值 ignore changes made on another thread,但我想知道它的变体。线程是否可能更改缓存值,然后该值永远不会离开其缓存,因此对其他线程永远不可见?
例如,此代码是否可以打印 "Flag is true",因为线程 a
永远看不到线程 b
对 [=13 所做的更改=]? (我不能让它这样做,但我无法证明它或它的某些变体不会。)
var flag = true;
var a = new Thread(() => {
Thread.Sleep(200);
Console.WriteLine($"Flag is {flag}");
});
var b = new Thread(() => {
flag = false;
while (true) {
// Do something to avoid a memory barrier
}
});
a.Start();
b.Start();
a.Join();
我可以想象线程 b
flag
可以缓存在 CPU 寄存器中然后设置为 false
,当 b
进入 while
循环它从来没有机会(或从不关心)将 flag
的值写回内存,因此 a
总是将 flag
视为 true。
从 this answer 中列出的内存屏障生成器来看,在我看来,这在理论上是可行的。我对么?我无法在实践中证明这一点。任何人都可以想出一个例子吗?
我不完全确定这是否能回答您的问题,但我可以。
如果您 运行 以下代码的发布(非调试)版本,它将永远不会终止,因为 waitForFlag()
永远不会看到 flag
的更改版本。
但是,如果您注释掉任何指定的行,程序将终止。
看起来在 while (flag)
循环中调用外部库会导致优化器不缓存 flag
的值。
另外(当然)使 flag
易变会阻止这种优化。
using System;
using System.Threading;
using System.Threading.Tasks;
namespace Demo
{
class Program
{
void run()
{
Task.Factory.StartNew(resetFlagAfter1s);
var waiter = Task.Factory.StartNew(waitForFlag);
waiter.Wait();
Console.WriteLine("Done");
}
void resetFlagAfter1s()
{
Thread.Sleep(1000);
flag = false;
}
void waitForFlag()
{
int x = 0;
while (flag)
{
++x;
// Uncommenting this line make this thread see the changed value of flag.
// Console.WriteLine("Spinning");
}
}
// Uncommenting "volatile" makes waitForFlag() see the changed value of flag.
/*volatile*/ bool flag = true;
static void Main()
{
new Program().run();
}
}
}
研究Thread.MemoryBarrier,你就会变成金子。
按照从最容易正确到最容易搞砸的顺序排列
- reading/writing 时使用锁
- 使用
Interlocked
class 上的功能
- 使用内存屏障
Is it possible for a thread to change a cached value, which then never leaves its cache and so is never visible to other threads?
如果我们从字面上谈论硬件缓存,那么我们需要讨论特定的处理器系列。如果您在 x86(和 x64)上工作(看起来很可能),您需要知道这些处理器实际上具有比 .NET 所需的内存模型强得多的内存模型。在 x86 系统中,缓存保持一致性,因此其他处理器不会忽略任何写入。
如果我们谈论优化,其中特定内存位置已被读入处理器寄存器,然后 后续 从内存读取只是重用该寄存器,那么就没有' 写入端的类似类似物。您会注意到,在我们假设没有其他任何东西改变该内存位置并且我们可以重用该寄存器之前,总是至少有一个从实际内存位置读取。
在写入方面,我们被告知要将某些内容推送到特定的内存位置。我们必须至少推送到该位置一次,并且始终将先前已知的值存储在该位置(特别是如果我们的线程从未从中读取)在一个单独的寄存器中只是为了能够执行比较可能是一种去优化并省略写入操作。