19

假设我有一个控制某个循环执行的字段:

private static bool shouldRun = true;

我有一个线程正在运行,它的代码如下:

while(shouldRun) 
{
    // Do some work ....
    Thread.MemoryBarrier();
}

现在,另一个线程可能设置shouldRunfalse,而不使用任何同步机制。

据我了解 Thread.MemoryBarrier(),在 while 循环中进行此调用将阻止我的工作线程获取 的缓存版本shouldRun,并有效地防止发生无限循环。

我对 Thread.MemoryBarrier 的理解是否正确?鉴于我有可以设置shouldRun变量的线程(这不容易更改),这是确保我的循环一旦shouldRun被任何线程设置为 false 就会停止的合理方法吗?

4

5 回答 5

26

这是对 Thread.MemoryBarrier() 的正确使用吗?

不。假设一个线程在循环开始执行之前设置了标志。循环仍然可以使用标志的缓存值执行一次。那是正确的吗?这对我来说肯定是不正确的。我希望如果我在第一次执行循环之前设置标志,循环执行零次,而不是一次。

据我了解 Thread.MemoryBarrier(),在 while 循环中进行此调用将阻止我的工作线程获取 shouldRun 的缓存版本,并有效地防止发生无限循环。我对 Thread.MemoryBarrier 的理解是否正确?

内存屏障将确保处理器不会对读取和写入进行任何重新排序,以便实际观察到逻辑上在屏障之前的内存访问在它之后,反之亦然。

如果你下定决心要做低锁代码,我会倾向于使该字段易失,而不是引入显式的内存屏障。“volatile”C# 语言的一个特性。一个危险且难以理解的特性,但却是该语言的一个特性。它清楚地向代码的读者传达了所讨论的字段将在多个线程上不加锁的情况下使用。

这是确保一旦任何线程将 shouldRun 设置为 false 后我的循环将停止的合理方法吗?

有些人会认为这是合理的。如果没有非常非常好的理由,我不会在我自己的代码中这样做。

通常,低锁定技术是出于性能考虑而证明的。有两个这样的考虑:

首先,竞争锁可能非常慢;只要锁中有代码在执行,它就会阻塞。如果您因为争用太多而遇到性能问题,那么我会首先尝试通过消除争用来解决问题。只有当我无法消除争用时,我才会采用低锁定技术。

其次,可能是非竞争锁太慢了。如果您在循环中执行的“工作”花费的时间少于 200 纳秒,那么检查无争用锁所需的时间(大约 20 ns)是工作时间的很大一部分。在这种情况下,我建议您在每个循环中做更多的工作。真的有必要在设置控制标志后的 200 ns 内停止循环吗?

只有在最极端的性能场景中,我才能想象检查非竞争锁的成本是程序所花费时间的很大一部分。

而且,当然,如果你每 200 ns 左右就引入一个内存屏障,你也可能以其他方式破坏性能。处理器希望为您进行那些移动内存访问的实时优化;如果你强迫它不断地放弃这些优化,你就错失了潜在的胜利。

于 2012-01-04T16:34:10.137 回答
7

我相信你的理解有点偏离这条线

据我了解 Thread.MemoryBarrier(),在 while 循环中进行此调用将阻止我的工作线程获取 shouldRun 的缓存版本,并有效地防止发生无限循环。

内存屏障是对读/写指令执行排序约束的一种方式。虽然读/写重新排序的结果可能看起来像缓存内存屏障,但实际上并不会以任何方式影响缓存。它只是充当读写指令无法跨越的栅栏。

这很可能不会阻止无限循环。内存栅栏正在做的是这种情况是强制循环体中发生的所有读取和写入发生在shouldRun循环条件读取的值之前

Wikipedia 很好地介绍了内存屏障,您可能会发现它很有用

于 2012-01-04T15:56:28.810 回答
3

这不是问题的直接答案;但是,上述帖子似乎很好地涵盖了这一点。似乎没有明确说明的是如何达到预期的结果。仅使用预期的线程同步对象确实没有错:

private static readonly ManualResetEvent shutdownEvent = new ManualResetEvent(false);

//Thread 1 - continue until event is signaled
while(!shutodwnEvent.WaitOne(0)) 
{
    // Do some work ....
    Thread.MemoryBarrier();
}

//Thread 2 - signal the other thread
shutdownEvent.Set();

另一种方法是在访问变量时使用 lock 语句:

private static object syncRoot = new Object();
private static bool shouldRun = true;

//Thread 1
bool bContinue;
lock(syncRoot)
    bContinue = shouldRun;

while(bContinue) 
{
    // Do some work ....
    lock(syncRoot)
        bContinue = shouldRun;
}

//Thread 2
lock(syncRoot)
    shouldRun = false;
于 2012-01-04T17:27:33.547 回答
3

根据您要执行的操作,这可能不会达到您的预期。来自MSDN,MemoryBarrier:

同步内存访问如下:执行当前线程的处理器不能以这样的方式重新排序指令,即调用 MemoryBarrier 之前的内存访问在调用 MemoryBarrier 之后的内存访问之后执行。

这将防止在内存屏障调用之前对指令进行重新排序,但这不会阻止线程调度决定循环应该在编写器线程实际执行写入之前进行另一轮循环,例如它不是锁定或同步机制。它只是防止循环中的其他指令(如变量初始化)在检查 shouldRun 的值 之前重新排序。

同样,忽略它不会导致无限循环——在任何一种情况下,每次迭代都会检查 shouldRun。这里没有“缓存值”。

于 2012-01-04T15:50:22.513 回答
2

在这种情况下Thread.MemoryBarrier();,是一种性能较差但安全性较低的变体

private static volatile readonly bool shouldRun = true;

因为如果需要实现最新读取,将 shouldRun 声明为 volatile 将执行内存屏障,但可能不需要这样做。

于 2012-01-04T15:44:25.253 回答