C++ C++;11使用<;原子>;
我实现了SpinLock类,如下所示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
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的东西: