我的自旋锁实现是否正确且最佳?

时间:2009-09-05 13:45:53

标签: c multithreading concurrency mutual-exclusion spinlock

我正在使用自旋锁来保护非常小的关键部分。争用很少发生非常,因此自旋锁比常规互斥锁更合适。

我目前的代码如下,并假定为x86和GCC:

volatile int exclusion = 0;

void lock() {
    while (__sync_lock_test_and_set(&exclusion, 1)) {
        // Do nothing. This GCC builtin instruction
        // ensures memory barrier.
    }
}

void unlock() {
    __sync_synchronize(); // Memory barrier.
    exclusion = 0;
}

所以我想知道:

  • 这段代码是否正确?它是否正确地确保了相互排斥?
  • 它适用于所有x86操作系统吗?
  • 它也适用于x86_64吗?在所有操作系统上?
  • 这是最佳的吗?
    • 我见过使用比较和交换的自旋锁实现,但我不确定哪个更好。
    • 根据GCC原子内置文档(http://gcc.gnu.org/onlinedocs/gcc-4.1.2/gcc/Atomic-Builtins.html),还有__sync_lock_release。我不是记忆障碍方面的专家,所以我不确定我是否可以使用此代替__sync_synchronize
    • 我正在针对没有争用的情况进行优化。

我根本不关心关于争用。可能有1个,也许有2个其他线程试图每隔锁定一次自旋锁。

10 个答案:

答案 0 :(得分:20)

对我来说很好看。顺便说一句,这是textbook实现,即使在竞争情况下也更有效。

void lock(volatile int *exclusion)
{
    while (__sync_lock_test_and_set(exclusion, 1))
        while (*exclusion)
            ;
}

答案 1 :(得分:18)

所以我想知道:

* Is it correct?

在上述情况中,我会说是。

* Is it optimal?

这是一个有问题的问题。通过重新发明轮子,你也在重新发明许多其他实现已经解决的问题

  • 我不希望在您尝试访问锁定字时出现故障。

  • 在解锁时使用完整屏障只需要具有释放语义(这就是为什么你要使用__sync_lock_release,这样你就可以在itanium而不是mf上获得st1.rel,或者在powerpc上使用lwsync, ...)。如果你真的只关心x86或x86_64这里使用的障碍类型并不重要(但是如果你在哪里跳转到intel的itanium用于HP-IPF端口那么你就不会想要这个)。

  • 您没有在废物循环之前通常放置的pause()指令。

  • 当存在争用时,你想要某事,semop,甚至是绝望中的愚蠢睡眠。如果你真的需要这个购买的性能,那么futex建议可能是一个很好的建议。如果你需要这个性能,那么你就会非常糟糕地维护这段代码,你需要进行大量的研究。

请注意,有评论说不需要发布屏障。即使在x86上也是如此,因为释放屏障也可以作为编译器的指令,以便不对“屏障”周围的其他内存访问进行混乱。非常像你使用 asm (“”:::“记忆”)时会得到的。

* on compare and swap

在x86上,sync_lock_test_and_set将映射到具有隐含锁定前缀的xchg指令。绝对是最紧凑的生成代码(特别是如果你使用一个字节作为“锁定字”而不是int),但不比使用LOCK CMPXCHG更正确。比较和交换的使用可以用于更好的algorthims(比如在失败时将第一个“服务员”的元数据的非零指针放入锁定词)。

答案 2 :(得分:4)

回答你的问题:

  1. 对我好像
  2. 假设操作系统支持GCC(并且GCC具有实现的功能);这适用于所有x86操作系统。 GCC文档表明如果在给定平台上不支持它们,将产生警告。
  3. 这里没有具体的x86-64,所以我不明白为什么不这样做。这可以扩展到涵盖GCC支持的任何架构,但是在非x86架构上可能有更多的最佳实现方法。
  4. __sync_lock_release()案例中使用unlock()可能稍微好一点;因为这将减少锁定并在单个操作中添加内存屏障。但是,假设您的断言很少会出现争用;它看起来不错。

答案 3 :(得分:3)

如果您使用的是最新版本的Linux,则可以使用futex - “快速用户空间互斥”:

  

正确编程的基于futex的锁定不会使用系统调用,除非争用锁定

在无争议的情况下,您尝试使用自旋锁进行优化,futex的行为就像一个自旋锁,而不需要内核系统调用。如果锁有争议,则等待在内核中进行,而不是忙等待。

答案 4 :(得分:3)

我想知道以下CAS实现是否在x86_64上是正确的。 我的i7 X920笔记本电脑上快了两倍(fedora 13 x86_64,gcc 4.4.5)。

inline void lock(volatile int *locked) {
    while (__sync_val_compare_and_swap(locked, 0, 1));
    asm volatile("lfence" ::: "memory");
}
inline void unlock(volatile int *locked) {
    *locked=0;
    asm volatile("sfence" ::: "memory");
}

答案 5 :(得分:2)

我无法对正确性发表评论,但在我阅读问题正文之前,你问题的标题引起了一个红旗。同步原语非常难以确保正确性......如果可能的话,最好使用精心设计/维护的库,可能是pthreadsboost::thread

答案 6 :(得分:0)

建议使用TATAS(test-and-test-and-set)进行一项改进。对于处理器来说,使用CAS操作被认为是非常昂贵的,因此如果可能的话,最好避免使用它们。 另一件事,确保您不会遭受优先级倒置(如果具有高优先级的线程尝试获取锁定而低优先级的线程尝试释放锁定会怎样?在Windows上,例如此问题最终将通过解决方案解决调度程序使用优先级提升,但是你可以明确地放弃线程的时间片,以防你在最近20次尝试中没有成功获取锁(例如..)

答案 7 :(得分:0)

您的解锁程序不需要内存屏障;只要它在x86上对齐,对排除的赋值就是原子的。

答案 8 :(得分:0)

在x86(32/64)的特定情况下,我认为在解锁代码中根本不需要内存栅栏。 x86不进行任何重新排序,除了存储首先放入存储缓冲区,因此它们变得可见,可以延迟其他线程。执行存储然后从同一变量读取的线程将从其存储缓冲区中读取(如果尚未刷新到内存中)。所以你需要的是一个asm语句来防止编译器重新排序。从其他线程的角度来看,你冒着一个线程持有锁的时间略长于一定程度的风险,但如果你不关心无关紧要的争用。事实上,pthread_spin_unlock就像在我的系统(linux x86_64)上实现的那样。

我的系统还使用pthread_spin_lock实现lock decl lockvar; jne spinloop;而不是使用xchg(这是__sync_lock_test_and_set使用的),但我不知道是否确实存在性能差异。

答案 9 :(得分:0)

有一些错误的假设。

首先,只有当ressource锁定在另一个CPU上时,SpinLock才有意义。如果ressource被锁定在同一个CPU上(在单处理器系统上总是如此),则需要放松调度程序以解锁ressource。您当前的代码将在单处理器系统上运行,因为调度程序将自动切换任务,但这会浪费资源。

在多处理器系统上,同样的事情可能发生,但任务可能会从一个CPU迁移到另一个CPU。简而言之,如果您保证您的任务将在不同的CPU上运行,则使用自旋锁是正确的。

其次,当解锁时,锁定互斥锁IS(与自旋锁一样快)。只有当互斥锁已被锁定时,互斥锁锁定(和解锁)才会缓慢(非常慢)。

因此,在您的情况下,我建议使用互斥锁。