C++ C++;11使用<;原子>;

C++ C++;11使用<;原子>;,c++,multithreading,c++11,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(unla

我实现了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()方法1000万次

不幸的是,结果不是2000万。
我错过了什么

问题在于
compare\u exchange\u-weak
unlocketed
变量失败时更新该变量。从
compare\u exchange\u weak
的文档中:

将原子对象包含的值的内容与 预期: -如果为true,则将包含的值替换为val(类似于store)。 -如果为false,则用包含的值替换预期值。

即,在第一次失败后,
比较交换弱
解锁
将更新为
,因此下一次循环迭代将尝试
比较交换弱
。此操作成功,您刚刚获得了另一个线程持有的锁

解决方案: 确保在每次
compare\u exchange\u weak
之前将
unlocketed
设置回
false
,例如:

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

正如@gexicide所提到的,问题在于
compare\u exchange
函数用原子变量的当前值更新
预期的
变量。这也是为什么必须首先使用局部变量
unlocketd
的原因。要解决这个问题,您可以在每次循环迭代中将
unlocketd
设置回false

但是,与其使用
compare\u-exchange
来处理其接口不太适合的事情,不如使用
std::atomic\u-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);
    }
};
资料来源:

手动指定内存顺序只是一个很小的潜在性能调整,这是我从源代码复制的。如果简单性比最后一点性能更重要,那么您可以坚持使用默认值,只需调用
locked.test\u和\u set()/locked.clear()

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

更新:正如@David Schwartz、@Anton和@Technik Empire在评论中解释的那样,空循环会产生一些不良影响,如分支错误预测、HT处理器上的线程不足和过高的功耗,因此简言之,这是一种非常低效的等待方式。影响和解决方案是特定于体系结构、平台和应用程序的。我不是专家,但通常的解决方案似乎是在linux上或windows上添加一个
cpu\u relax()
到循环体


EDIT2:需要明确的是:这里介绍的便携式版本(没有特殊的cpu\u relax等指令)对于许多应用程序来说应该已经足够好了。如果您的
SpinLock
旋转很多,因为其他人长时间持有该锁(这可能已经表明存在一般设计问题),那么最好还是使用普通的互斥锁。

+1,并注意操作,节点
的默认构造函数将不会如所示初始化
数字
。即
节点
不会在
节点中留下确定性值。number
开始序列。您需要一个这样用法的构造函数,
Node():number(){}
就足够了。谢谢你的评论!!我发现了我的游泳池错误!谢谢请注意,循环还需要
\umm\u pause()
来启用超线程。请注意,这是您可能实现的最差自旋锁。在其他问题中:1)当您最终获得
锁时,您将所有预测失误分支的母分支从
while
循环中移除,这可能是最糟糕的时间。2)
lock
函数可能会使在超线程CPU上的同一虚拟内核中运行的另一个线程饥饿。@DavidSchwartz,感谢您的评论。关于你提到的问题,我可以问更多吗?2). 这个锁函数可能会使另一个线程陷入饥饿状态(没错,我确信锁的生命周期很短,所以它就是这么做的)。我可以用一些自旋计数器来解决这个问题,对吗?1). 为什么我要在这段代码中使用“所有预测失误的brabches之母”,以及如何改进它??你对此有何评论?再次感谢您,因为它可以防止投机性执行,消除分支预测失误的惩罚。(顺便说一句,如果你对这类东西一无所知,你就没必要写旋转锁。你会犯每一个错误,甚至没有意识到你还有其他选择。)@DavidSchwartz,我对写这类东西很感兴趣,但我对这类东西一无所知。你能推荐一个人如何让自己从里到外了解这一点吗?@DavidSchwartz,听起来要想有足够的知识来尝试编写这种代码,唯一的办法就是尝试编写这种代码。只需在while循环中添加一个std::this_thread::yield调用:@Martin:我想,但是
std::this_-thread::yield()
是一个相当繁重的系统调用,所以我不确定是否将它放在自旋锁的循环体中。我(未经测试)的假设是,在大多数情况下,如果可以的话,您可能希望首先使用std::mutex(或类似工具)。@MartinGerhardy在自旋锁中使用yield没有任何意义。自旋锁的目标是避免在小的关键部分使用昂贵的上下文切换。@rox这不是我的观点。自旋互斥锁的技术定义是,如果其他人已经获取了CPU,则不会释放CPU的锁。在单核系统中,通常不使用自旋锁,而是使用常规互斥锁。@JorgeBellón据我所知,glibc实现使用类似于yield的东西: