基于原子操作的自旋锁的 Unlock 可以直接将锁标志设置为零吗?
Posted
技术标签:
【中文标题】基于原子操作的自旋锁的 Unlock 可以直接将锁标志设置为零吗?【英文标题】:Can atomic ops based spin lock's Unlock directly set the lock flag to zero? 【发布时间】:2015-12-15 22:58:50 【问题描述】:例如,我有一个独占的基于原子操作的自旋锁实现,如下所示:
bool TryLock(volatile TInt32 * pFlag)
return !(AtomicOps::Exchange32(pFlag, 1) == 1);
void Lock (volatile TInt32 * pFlag)
while (AtomicOps::Exchange32(pFlag, 1) == 1)
AtomicOps::ThreadYield();
void Unlock (volatile TInt32 * pFlag)
*pFlag = 0; // is this ok? or here as well a atomicity is needed for load and store
AtomicOps::Exchange32
在 windows 上使用 InterlockedExchange
实现,在 linux 上使用 __atomic_exchange_n
实现。
【问题讨论】:
相关问题:***.com/questions/1383363/…***.com/questions/6810733/…***.com/questions/26307071/… 您能详细说明一下吗?为什么在这里,我需要一个内存屏障?如果我不使用,出了什么问题以及怎么办?Lock()
需要一个“获取屏障”以确保在自旋锁锁定时所做的所有更改将仅在 pFlag
更新后应用。 Unlock()
需要“释放屏障”以确保在自旋锁被锁定时所做的所有更改都将在 pFlag
更新之前应用。在此处查看详细信息:jfdube.wordpress.com/2012/03/08/understanding-memory-ordering。这是一种通用方法,但在 x86 上,您只需要编译器屏障,而不需要获取和释放屏障;看这里:preshing.com/20120913/acquire-and-release-semantics
我将在完整答案中添加更多详细信息。
另请参阅我对***.com/questions/20446982/… 的回答中关于锁的评论。
【参考方案1】:
在大多数情况下,为了释放资源,只需将锁重置为零(如您所做的那样)几乎可以(例如在英特尔酷睿处理器上),但您还需要确保编译器不会交换指令(请参阅下面,另见 gv 的帖子)。如果你想严谨(和便携),有两件事需要考虑:
编译器的作用:它可能会交换指令以优化代码,因此如果它不“意识到”代码的多线程性质,就会引入一些细微的错误。为避免这种情况,可以插入编译器屏障。
处理器的作用:某些处理器(如用于专业服务器的 Intel Itanium 或用于智能手机的 ARM 处理器)具有所谓的“宽松内存模型”。在实践中,这意味着处理器可以决定改变操作的顺序。同样,这可以通过使用特殊指令(加载屏障和存储屏障)来避免。例如,在 ARM 处理器中,指令 DMB 确保所有存储操作在下一条指令之前完成(并且需要插入到释放锁的函数中)
结论:如果您对这些功能有一些编译器/操作系统支持(例如,stdatomics.h
或 C++0x 中的std::atomic
),使代码正确是非常棘手的,依赖它们比自己编写要好得多(但有时你别无选择)。在标准英特尔酷睿处理器的特定情况下,我认为您所做的是正确的,只要您在发布操作中插入编译器屏障(参见 g-v 的帖子)。
关于编译时与运行时内存排序,请参阅:https://en.wikipedia.org/wiki/Memory_ordering
我在不同架构上实现的一些原子/自旋锁的代码: http://alice.loria.fr/software/geogram/doc/html/atomics_8h.html (但我不确定它是否 100% 正确)
【讨论】:
“在标准英特尔酷睿处理器的具体情况下,我认为你所做的是正确的。” -- 正如 cmets 和我的回答中提到的,它是 not 没有编译器障碍(volatile
在这里还不够)。使用编译器屏障,它对于 x86 是正确的。【参考方案2】:
在自旋锁实现中需要两个内存屏障:
TryLock()
和Lock()
中的“获取障碍”或“导入障碍”。它强制在获取自旋锁时发出的操作仅在 pFlag
值更新后才可见。
Unlock()
中的“释放障碍”或“出口障碍”。它强制在释放自旋锁之前发出的操作在 pFlag
值更新之前可见。
出于同样的原因,您还需要两个编译器屏障。
详情请见this article。
这种方法适用于一般情况。在x86/64
:
更多详情请见here。
以下是使用GCC atomic builtins 的示例实现。它适用于 GCC 支持的所有架构:
它将在需要的架构上插入获取/释放内存屏障(如果不支持获取/释放屏障但架构是弱排序的,则插入完整屏障); 它将在所有架构上插入编译器屏障。代码:
bool TryLock(volatile bool* pFlag)
// acquire memory barrier and compiler barrier
return !__atomic_test_and_set(pFlag, __ATOMIC_ACQUIRE);
void Lock(volatile bool* pFlag)
for (;;)
// acquire memory barrier and compiler barrier
if (!__atomic_test_and_set(pFlag, __ATOMIC_ACQUIRE))
return;
// relaxed waiting, usually no memory barriers (optional)
while (__atomic_load_n(pFlag, __ATOMIC_RELAXED))
CPU_RELAX();
void Unlock(volatile bool* pFlag)
// release memory barrier and compiler barrier
__atomic_clear(pFlag, __ATOMIC_RELEASE);
对于“轻松等待”循环,请参阅this 和this 问题。
另请参阅 Linux kernel memory barriers 作为一个很好的参考。
在您的实施中:
Lock()
调用 AtomicOps::Exchange32()
已经包含编译器屏障,可能还有获取或完整内存屏障(我们不知道,因为您没有向 __atomic_exchange_n()
提供实际参数)。
Unlock()
错过了内存和编译器障碍,因此它被破坏了。
如果可以的话,也可以考虑使用pthread_spin_lock()
。
【讨论】:
“优化障碍”是指编译器障碍吗?答案已经提到编译器障碍总是需要的,但我会添加一个澄清。 如何保证“有效等待”循环最终会在没有获取障碍的情况下退出? @5gon12eder,volatile
还不够吗?在没有内存屏障的情况下,是否存在一个 CPU 的写入对另一个 CPU 不可见的架构? (所以它们不提供缓存一致性?)
我不知道这种架构是否存在,但就语言而言,我认为它不安全。假设this answer是正确的,看来Intel也不推荐了。
我发了一个问题:***.com/questions/32677667/…以上是关于基于原子操作的自旋锁的 Unlock 可以直接将锁标志设置为零吗?的主要内容,如果未能解决你的问题,请参考以下文章