我实现了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);
}
};我实现了上面的类,并创建了两个线程,每个线程调用Node类的同一实例的add()方法1000万次。
不幸的是,结果不是2000万。这里我漏掉了什么?
发布于 2014-10-27 16:24:55
问题是一旦失败,compare_exchange_weak就会更新unlatched变量。来自compare_exchange_weak的文档
将原子对象包含的值的内容与期望的值进行比较:-如果为真,它将包含的值替换为val (如store)。-如果为false,则将expected替换为包含的值。
也就是说,在第一次失败的compare_exchange_weak之后,unlatched将更新为true,因此下一次循环迭代将尝试使用true compare_exchange_weak true。这成功了,并且您刚刚获得了一个由另一个线程持有的锁。
解决方案:确保在每个compare_exchange_weak之前将unlatched设置回false,例如:
while(!latch.compare_exchange_weak(unlatched, true, std::memory_order_acquire)) {
unlatched = false;
}发布于 2015-03-22 22:08:39
正如@gexicide所提到的,问题在于compare_exchange函数使用原子变量的当前值更新expected变量。这也是为什么必须首先使用局部变量unlatched的原因。要解决这个问题,可以在每次循环迭代中将unlatched设置回false。
但是,使用std::atomic_flag要简单得多,而不是使用compare_exchange来处理它的接口不太适合的事情:
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);
}
};来源:cppreference
手动指定内存顺序只是一个次要的潜在性能调整,这是我从源代码复制的。如果简单性比最后一点性能更重要,那么您可以坚持使用默认值,只需调用locked.test_and_set() / locked.clear()。
顺便说一句:std::atomic_flag是唯一可以保证是无锁的类型,尽管我不知道有任何平台,在std::atomic_bool上的操作不是无锁的。
更新:正如@David Schwartz,@Anton和@Technik Empire在评论中所解释的那样,空循环有一些不受欢迎的影响,比如分支错误,HT处理器上的线程饥饿和过高的功耗-所以简而言之,这是一种非常低效的等待方式。影响和解决方案取决于体系结构、平台和应用程序。我不是专家,但通常的解决方案似乎是将linux上的cpu_relax()或windows上的YieldProcessor()添加到循环体中。
EDIT2:只是想说清楚:这里介绍的可移植版本(没有特殊的cpu_relax等指令)应该已经足够好用于许多应用程序了。如果您的SpinLock因为其他人长时间持有锁而频繁旋转(这可能已经表明存在一般的设计问题),那么使用普通的互斥锁可能会更好。
https://stackoverflow.com/questions/26583433
复制相似问题