我在boost::smart_ptr
中找到了以下自旋锁代码:
bool try_lock()
{
return (__sync_lock_test_and_set(&v_, 1) == 0);
}
void lock()
{
for (unsigned k=0; !try_lock(); ++k)
{
if (k<4)
; // spin
else if (k < 16)
__asm__ __volatile__("pause"); // was ("rep; nop" ::: "memory")
else if (k < 32 || k & 1)
sched_yield();
else
{
struct timespec rqtp;
rqtp.tv_sec = 0;
rqtp.tv_nsec = 100;
nanosleep(&rqtp, 0);
}
}
}
void unlock()
{
__sync_lock_release(&v_);
}
因此,如果我理解正确,当锁争用时,传入的线程将以指数方式退避,首先疯狂地旋转,然后暂停,然后产生剩余的时间片,最后在睡眠和屈服之间翻转。
我还找到了glibc pthread_spinlock
实现,它使用程序集来执行锁定。
#define LOCK_PREFIX "lock;" // using an SMP machine
int pthread_spin_lock(pthread_spinlock_t *lock)
{
__asm__ ("\n"
"1:\t" LOCK_PREFIX "decl %0\n\t"
"jne 2f\n\t"
".subsection 1\n\t"
".align 16\n"
"2:\trep; nop\n\t"
"cmpl $0, %0\n\t"
"jg 1b\n\t"
"jmp 2b\n\t"
".previous"
: "=m" (*lock)
: "m" (*lock));
return 0;
}
我承认我对装配的理解不是很好,所以我不完全理解这里发生的事情。 (有人可以解释一下这是做什么的吗?)
但是,我针对boost spinlock和glibc pthread_spinlock运行了一些测试,当有多个内核而不是线程时,boost代码优于glibc代码。
另一方面,当线程多于核心时,glibc代码更好。
这是为什么?这两个spinlock实现之间的区别是什么使它们在每个场景中的表现不同?
答案 0 :(得分:5)
您在哪里获得了问题中发布的pthread_spin_lock()
实施?它似乎缺少几条重要的界限。
我看到的实现(不是内联汇编 - 它是来自glibc/nptl/sysdeps/i386/pthread_spin_lock.S
的独立汇编源文件)看起来很相似,但还有两个额外的关键指令:
#include <lowlevellock.h>
.globl pthread_spin_lock
.type pthread_spin_lock,@function
.align 16
pthread_spin_lock:
mov 4(%esp), %eax
1: LOCK
decl 0(%eax)
jne 2f
xor %eax, %eax
ret
.align 16
2: rep
nop
cmpl $0, 0(%eax)
jg 1b
jmp 2b
.size pthread_spin_lock,.-pthread_spin_lock
它递减传入的参数指向的long
,如果结果为零则返回。
否则,结果为非零,这意味着该线程没有获得锁定。因此它执行rep nop
,这相当于pause
指令。这是一个“特殊”的nop,它向CPU提示线程处于旋转状态,并且cpu应该以某种方式处理内存排序和/或分支prdiction,以提高这些情况下的性能(我不假装准确理解在芯片封面下会发生什么不同 - 从软件的角度来看,与普通的nop
没有区别。
在pause
之后它再次检查该值 - 如果它大于零,则锁定无人认领,因此它跳转到函数的顶部并尝试再次声明锁定。否则,它会再次跳转到pause
。
这个自旋锁和Boost版本之间的主要区别在于,当它旋转时,这个版本永远不会比pause
做任何更好的事情 - 没有像sched_yield()
或nanosleep()
那样的东西。所以线程保持热点。我不确定这在你记下的两个行为中是如何起作用的,但是glibc代码会更贪婪 - 如果一个线程在锁上旋转并且有其他线程准备好运行但是没有可用的核心,则旋转的线程不会帮助等待线程获得任何cpu时间,而Boost版本最终会自动为等待关注的线程让路。