C++ 11使用<atomic>实现Spinlock

syk*_*yko 30 c++ multithreading 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);
    }
};
Run Code Online (Sandbox Code Playgroud)

我实现了上面的类,并创建了两个线程,每个线程调用一个相同的Node类实例的add()方法1000万次.

不幸的是,结果不是2000万.我在这里错过了什么?

gex*_*ide 41

问题是一旦失败就compare_exchange_weak更新unlatched变量.来自以下文件compare_exchange_weak:

将原子对象包含的值的内容与预期值进行比较: - 如果为true,则用val替换包含的值(如store). - 如果为false,则将其替换为包含的值.

即,在第一次失败后compare_exchange_weak,unlatched将更新为true,因此下一次循环迭代将尝试compare_exchange_weak true使用true.这成功了,你只是拿了一个由另一个线程持有的锁.

解决方案:确保设置unlatchedfalse各自之前compare_exchange_weak,例如:

while(!latch.compare_exchange_weak(unlatched, true, std::memory_order_acquire)) {
    unlatched = false;
}
Run Code Online (Sandbox Code Playgroud)

  • +1,并注意OP,"Node"的默认构造函数将*not*value-initialize`number` as-present.即`节点节点;`不会在`node.number`中留下确定性值来启动序列.你需要一个这种用法的构造函数,`Node():number(){}`就足够了.[**看到它**](http://coliru.stacked-crooked.com/a/92d79ddfb574ba70) (4认同)
  • 请注意,循环还需要`__mm_pause()`来启用超线程. (4认同)

Mik*_*eMB 34

正如@gexicide所提到的,问题是compare_exchange函数expected使用原子变量的当前值更新变量.这也是为什么你必须首先使用局部变量的原因unlatched.要解决此问题,您可以unlatched在每次循环迭代中将其设置为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);
    }
};
Run Code Online (Sandbox Code Playgroud)

资料来源:cppreference

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

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

更新:正如@David Schwartz,@ Anton和@Technik Empire的评论中所解释的那样,空循环有一些不良影响,如分支未预测,HT处理器上的线程饥饿和过高的功耗 - 简而言之,这是一个非常低效的等待的方式.影响和解决方案是架构,平台和应用程序特定的.我不是专家,但通常的解决方案似乎是cpu_relax() 在linux或YieldProcessor()Windows 上添加一个循环体.

EDIT2:为了清楚起见:这里提供的便携版本(没有特殊的cpu_relax等指令)应该足以满足许多应用程序的要求.如果你的SpinLock旋转很多,因为其他人持有锁很长时间(这可能已经表明一般的设计问题),最好还是使用普通的互斥锁.

  • @MartinGerhardy在自旋锁中使用yield是没有意义的.自旋锁的目标是避免在小关键部分中昂贵的上下文切换. (14认同)
  • 只需在while循环中添加一个std :: this_thread :: yield调用:http://en.cppreference.com/w/cpp/thread/yield (2认同)
  • @Martin:我虽然如此,但是`std :: this_thread :: yield()`是一个非常繁重的系统调用,所以我不确定,如果我把它放在一个自旋锁的循环体中.我(未经测试)的假设是,在大多数情况下,如果没有问题,你可以使用std :: mutex(或类似的)开头. (2认同)
  • @rox这不是我的观点.spin mutex的技术定义是一个锁,如果已经被其他人获取,它不会释放CPU.在单核系统中,您通常不使用自旋锁,而是使用常规互斥锁. (2认同)
  • @JorgeBellón 据我所知,glibc 实现使用类似于 yield 的东西:https://github.com/lattera/glibc/blob/master/mach/spin-solid.c。我不确定你在哪里找到定义的。 (2认同)