我实现了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万。我在这里错过了什么?
问题是compare_exchange_weak
一旦失败就会更新未锁定的
变量。从的留档compare_exchange_weak
:
将原子对象的包含值的内容与预期值进行比较:-如果为真,则将包含的值替换为val(如store)。-如果为假,则将预期替换为包含的值。
也就是说,在第一次失败的compare_exchange_weak
之后,未锁存的将被更新为true
,因此下一次循环迭代将尝试使用true
compare_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等说明)应该对许多应用程序来说已经足够好了。如果你的自旋锁因为别人长时间持有锁而旋转了很多(这可能已经表明了一般的设计问题),无论如何使用普通的互斥锁可能更好。