原子操作基于自旋锁的解锁可以直接将锁标志设置为零吗?

时间:2015-09-18 17:42:52

标签: linux multithreading atomicity memory-barriers spinlock

比如说,我有一个独立的基于原子操作的自旋锁实现,如下所示:

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,使用__atomic_exchange_n在Linux上实现{。}}。

2 个答案:

答案 0 :(得分:1)

在自旋锁实现中需要两个内存屏障:

  • 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);
}

对于“放松等待”循环,请参阅thisthis个问题。

另见Linux kernel memory barriers作为一个很好的参考。

在您的实施中:

  • Lock()调用AtomicOps::Exchange32()已经包含编译器屏障并且可能获取或完全内存屏障(我们不知道因为您没有向__atomic_exchange_n()提供实际参数)。
  • Unlock()错过了内存和编译器的障碍,因此它被破坏了。

如果是选项,还可以考虑使用pthread_spin_lock()

答案 1 :(得分:1)

在大多数情况下,为了释放资源,只需将锁​​重置为零(就像你一样)几乎没问题(例如在英特尔酷睿处理器上),但你还需要确保编译器不会交换指令(参见下面,另见g-v的帖子)。如果你想要严谨(和便携),有两件事需要考虑:

编译器的作用:它可以交换优化代码的指令,因此如果它不是"意识到它会引入一些微妙的错误。代码的多线程性质。为避免这种情况,可以插入编译器屏障。

处理器的作用:某些处理器(如专业服务器中使用的Intel Itanium或智能手机中使用的ARM处理器)具有所谓的“放松内存模型”。 。实际上,这意味着处理器可能决定改变操作的顺序。同样,这可以通过使用特殊指令(加载障碍和存储障碍)来避免。例如,在ARM处理器中,指令DMB确保在下一条指令之前完成所有存储操作(并且需要将其插入到释放锁的函数中)

结论:如果您对这些功能有一些编译器/操作系统支持(例如,stdatomics.h或C中的std::atomic,那么使代码更正是非常棘手的++ 0x),依靠它们比编写自己更好(但有时你别无选择)。在标准英特尔酷睿处理器的特定情况下,如果您在发布操作中插入编译器屏障,我认为您所做的是正确的(参见g-v'帖子)。

关于编译时与运行时内存排序,请参阅:https://en.wikipedia.org/wiki/Memory_ordering

我在不同架构上实现的一些原子/自旋锁的代码: http://alice.loria.fr/software/geogram/doc/html/atomics_8h.html (但我不确定它是否100%正确)