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:提议的修订看起来好多了。它显然可以避免由于总是编写sentinelm_
而导致的多核缓存一致性交互
在阅读了(如果你注意的话,你每天都可以学到一些新东西)和它所解决的多核缓存一致性问题之后,我很清楚,最初的代码试图做一些类似的事情,但没有理解其背后的微妙之处。在编写mu
时,删除冗余检查确实是安全的(假设所有调用方都遵守规则)。但是,每次循环迭代时,代码都会写入mus
,这会对每个核心都有缓存的真正多核芯片造成严重破坏
新的spinlock仍然容易受到第二个线程的攻击,即在不握住它的情况下释放它。这是无法修复的。我先前关于信任呼叫者遵守协议的说法仍然适用。除了2之外,您的所有版本都是正确的。此外,您关于在版本1中检查m_s==0
以及在版本3中降低性能的评论是正确的
减少的原因是T&S的实现方式,特别是它会导致每次调用都写入。这是因为