Multithreading 对自旋锁感到困惑吗

Multithreading 对自旋锁感到困惑吗,multithreading,language-agnostic,synchronization,locking,Multithreading,Language Agnostic,Synchronization,Locking,我读旋转锁代码,尤其是这部分 inline void Enter(void) { int prev_s; do { prev_s = TestAndSet(&m_s, 0); if (m_s == 0 && prev_s == 1) { break; } // reluinquish current timeslice (can only

我读旋转锁代码,尤其是这部分

inline void Enter(void)
{
    int prev_s;
    do
    {
        prev_s = TestAndSet(&m_s, 0);
        if (m_s == 0 && prev_s == 1)
        {
            break;
        }
        // reluinquish current timeslice (can only
        // be used when OS available and
        // we do NOT want to 'spin')
        // HWSleep(0);
    }
    while (true);
}
为什么我们需要测试两个条件m_s==0&&prev_s==1?我认为只要测试prev_s==1就足够了。有什么想法吗

编辑:版本2。如果有bug,我们应该用这种方式修复吗

inline void Enter(void)
{
    do
    {
        if (m_s == 0 && 1 == TestAndSet(&m_s, 0))
        {
            break;
        }
        // reluinquish current timeslice (can only
        // be used when OS available and
        // we do NOT want to 'spin')
        // HWSleep(0);
    }
    while (true);
}
编辑:第3版。我认为功能级别的版本3是正确的,但性能还不够好,因为每次我们都需要写,前面没有读测试。我的理解正确吗

inline void Enter(void)
{
    do
    {
        if (1 == TestAndSet(&m_s, 0))
        {
            break;
        }
        // reluinquish current timeslice (can only
        // be used when OS available and
        // we do NOT want to 'spin')
        // HWSleep(0);
    }
    while (true);
}
@蜻蜓,这是我的bug修复版本4(正如你所指出的,修复了版本2中的一个bug),你能检查一下它是否正确吗?谢谢

编辑:第4版

inline void Enter(void)
{
    do
    {
        if (m_s == 1 && 1 == TestAndSet(&m_s, 0))
        {
            break;
        }
        // reluinquish current timeslice (can only
        // be used when OS available and
        // we do NOT want to 'spin')
        // HWSleep(0);
    }
    while (true);
}

在我看来,这是一次尝试优化的尝试,但有点错误。我怀疑它是在尝试“TATAS”-“测试、测试和设置”,如果它能看到锁已经被占用,它甚至不会尝试进行测试和设置

在a中,Joe Duffy将此TATAS代码编写为:

class SpinLock {
    private volatile int m_taken;

    public void Enter() {
        while (true) {
            if (m_taken == 0 &&
                    Interlocked.Exchange(ref m_taken, 1) == 0)
                break;
        }
    }

    public void Exit() {
        m_taken = 0;
    }
}
(请注意,Joe使用1表示锁定,0表示解锁,这与代码项目示例不同-两者都可以,只是不要混淆!)

注意,这里对interlocated.Exchange的调用的条件是
m_take
为0。这减少了争用-避免了相对昂贵的(我猜)测试和设置操作,这是不必要的。我怀疑这就是作者的目的,但没有完全正确

“重大优化”一节中也提到了这一点:

要减少CPU间总线通信量,请在 没有获得锁,代码 应该循环阅读而不尝试 写任何东西,直到它读到 更改值。因为MESI缓存 协议,这会导致缓存线 使锁变为“共享”;然后 显然没有公共汽车来往 当CPU正在等待锁时。 这种优化在所有方面都是有效的 具有缓存的CPU体系结构 每个CPU,因为MESI是如此 无处不在

“循环读取”正是while循环所做的——直到它看到
m_发生变化之前,它只读取。当它看到变化时(即,当锁被释放时),它会再次尝试锁定


当然,我很有可能遗漏了一些重要的东西——像这样的问题非常微妙。

实际上,有人可能会调用
TestAndSet(&mus,1)
Leave()
从另一个线程在
TestAndSet(&mus,0)
之后和
之前调用
if
Enter()中进行测试。这样就不会获得锁,
mus
将不等于
0
。因此,需要进行此类检查。

为什么这两种情况都存在?因为在这种情况下,第二个线程也会获得锁。(编辑:但如果所有线程都遵循自旋锁协议,则不会发生这种情况。)

如果锁可用(发出信号)
m_s
的值为1。当被某个线程执行时,它的值为0。不允许使用其他值

考虑一个想要锁的线程,不管它是否在调用
Enter()
的线程不重要时发出信号。如果
m_s
为1,则允许使用锁,并将其更改为0。发生这种情况的第一次迭代会导致循环退出,线程拥有锁

现在考虑两个想要相同锁的线程。两者都在调用

TestAndSet()
,等待值1变为0。因为函数
TestAndSet()
是原子的,所以只有一个等待线程可以看到值1。所有其他线程仅将
mus
视为0,并且必须继续等待

在该线程中将
m_s
设置为0后,该条件为1,这意味着在原子操作和该条件之间有其他线程发出信号。因为一次只有一个线程应该有锁,所以看起来不应该发生不可能发生的事情

我猜这是为了满足自旋锁的不变承诺。(编辑:我不再那么肯定,下面还有更多…)如果按住,则
m_s
的值必须为零。如果不是,那就是其中之一。如果将它设置为零并没有“坚持”,那么会发生一些奇怪的事情,最好不要假设它现在由这个线程持有,因为不变量不是真的

编辑:Jon Skeet指出,这种情况可能是原始实现中的一个缺陷。我怀疑他是对的

被保护的争用条件针对的是无权向自旋锁发出信号的线程,不管怎样,它都会向自旋锁发出信号。如果您不能信任调用方遵守规则,那么毕竟,自旋锁可能不是首选的同步方法

编辑2:提议的修订看起来好多了。它显然可以避免由于总是编写sentinel
m_
而导致的多核缓存一致性交互

在阅读了(如果你注意的话,你每天都可以学到一些新东西)和它所解决的多核缓存一致性问题之后,我很清楚,最初的代码试图做一些类似的事情,但没有理解其背后的微妙之处。在编写
mu
时,删除冗余检查确实是安全的(假设所有调用方都遵守规则)。但是,每次循环迭代时,代码都会写入
mus
,这会对每个核心都有缓存的真正多核芯片造成严重破坏


新的spinlock仍然容易受到第二个线程的攻击,即在不握住它的情况下释放它。这是无法修复的。我先前关于信任呼叫者遵守协议的说法仍然适用。

除了2之外,您的所有版本都是正确的。此外,您关于在版本1中检查
m_s==0
以及在版本3中降低性能的评论是正确的

减少的原因是T&S的实现方式,特别是它会导致每次调用都写入。这是因为