提问者:小点点

C 11使用标头“<原子>”实现自旋锁


我实现了SpinLock类,如下所示

struct Node {
    int number;
    std::atomic_bool latch;

    void add() {
        lock();
        number++;
        unlock();
    }
    void lock() {
        bool unlatched = false;
        while(!latch.compare_exchange_weak(unlatched, true, std::memory_order_acquire));
    }
    void unlock() {
        latch.store(false , std::memory_order_release);
    }
};

我实现了上面的类和两个线程调用add()方法的同一实例的Node类1000万次每个线程。

结果很遗憾,2000万。我在这里错过了什么?


共2个答案

匿名用户

问题是compare_exchange_weak一旦失败就会更新未锁定的变量。从的留档compare_exchange_weak

将原子对象的包含值的内容与预期值进行比较:-如果为真,则将包含的值替换为val(如store)。-如果为假,则将预期替换为包含的值。

也就是说,在第一次失败的compare_exchange_weak之后,未锁存的将被更新为true,因此下一次循环迭代将尝试使用truecompare_exchange_weaktrue。这成功了,您只是获得了另一个线程持有的锁。

解决方案:确保在每个compare_exchange_weak之前将unlatch设置回false,例如:

while(!latch.compare_exchange_weak(unlatched, true, std::memory_order_acquire)) {
    unlatched = false;
}

匿名用户

正如@gecicide提到的,问题是compare_exchange函数用原子变量的当前值更新预期的变量。这也是为什么你必须首先使用局部变量unlatated的原因。为了解决这个问题,你可以在每次循环迭代中将unlatated设置回false。

然而,与其使用compare_exchange来处理它的接口不太适合的东西,不如使用std::atomic_flag来代替:

class SpinLock {
    std::atomic_flag locked = ATOMIC_FLAG_INIT ;
public:
    void lock() {
        while (locked.test_and_set(std::memory_order_acquire)) { ; }
    }
    void unlock() {
        locked.clear(std::memory_order_release);
    }
};

资料来源: cpprep

手动指定内存顺序只是一个小的潜在性能调整,这是我从源代码中复制的。如果简单性比最后一点性能更重要,您可以坚持默认值,只需调用锁定。test_and_set()/lock. Clear()

顺便说一句:std::atomic_flag是唯一保证无锁的类型,尽管我不知道任何平台,其中std::atomic_bool上的oparations不是无锁的。

更新:正如@大卫·施瓦茨、@安东和@技术帝国的评论中所解释的,空循环有一些不良影响,如分支错误扩展、HT处理器上的线程饥饿和过高的功耗——所以简而言之,这是一种非常低效的等待方式。影响和解决方案是特定于架构、平台和应用程序的。我不是专家,但通常的解决方案似乎是在linux上添加cpu_relax()或在windows上添加YieldProcator()到循环主体。

EDIT2:需要明确的是:这里介绍的便携式版本(没有特殊的cpu_relax等说明)应该对许多应用程序来说已经足够好了。如果你的自旋锁因为别人长时间持有锁而旋转了很多(这可能已经表明了一般的设计问题),无论如何使用普通的互斥锁可能更好。

相关问题