【发布时间】:2014-10-11 08:46:06
【问题描述】:
前言
我最近遇到了一些同步问题,这导致我去了spinlocks 和atomic counters。然后我又搜索了一下,这些是如何工作的,发现std::memory_order 和内存屏障(mfence、lfence 和sfence)。
所以现在看来,我应该对自旋锁使用 acquire/release,对计数器使用 relaxed。
一些参考
x86 MFENCE - Memory Fence
x86 LOCK - Assert LOCK# Signal
问题
默认情况下这三个操作(锁定 = test_and_set,解锁 = clear,增量 = operator++ = fetch_add)的机器码是什么(编辑:见下文) (seq_cst) 内存顺序和获取/释放/放松(按照这三个操作的顺序)。 有什么区别(哪些内存屏障在哪里)以及成本(多少 CPU 周期)?
目的
我只是想知道我的旧代码 (未指定内存顺序 = 使用的 seq_cst) 到底有多糟糕,我是否应该创建一些从 std::atomic 派生的 class atomic_counter 但使用 relaxed内存排序 (以及在某些地方使用获取/释放而不是互斥锁的良好自旋锁......或者使用来自boost库的东西 - 到目前为止我一直避免boost)。。 p>
我的知识
到目前为止,我确实了解自旋锁保护的不仅仅是自身(但也有一些共享资源/内存),因此,必须有一些东西可以使多个线程/内核的一些内存视图保持一致(就是那些获取/释放和内存栅栏)。原子计数器只为自己而存在,只需要那个原子增量(不涉及其他内存,我读它时并不真正关心它的值,它提供信息并且可能是几个周期旧的,没问题)。有一些 LOCK 前缀和一些像 xchg 这样的指令隐含地有它。我的知识到此结束,我不知道缓存和总线的真正工作原理以及背后的原理(但我知道 现代 CPU 可以重新排序指令,并行执行它们并使用内存缓存和一些同步)。 感谢您的解释。
PS:我现在有旧的 32 位电脑,只能看到 lock addl 和简单的 xchg,没有别的 - 所有版本看起来都一样(解锁除外),memory_order 对我的没有任何影响旧PC(解锁除外,发布使用move而不是xchg)。对于 64 位 PC 来说会是这样吗? (编辑:见下文)我必须关心内存顺序吗? (回答:不,不多,解锁时释放可以节省几个周期,仅此而已。)
代码:
#include <atomic>
using namespace std;
atomic_flag spinlock;
atomic<int> counter;
void inc1() {
counter++;
}
void inc2() {
counter.fetch_add(1, memory_order_relaxed);
}
void lock1() {
while(spinlock.test_and_set()) ;
}
void lock2() {
while(spinlock.test_and_set(memory_order_acquire)) ;
}
void unlock1() {
spinlock.clear();
}
void unlock2() {
spinlock.clear(memory_order_release);
}
int main() {
inc1();
inc2();
lock1();
unlock1();
lock2();
unlock2();
}
g++ -std=c++11 -O1 -S(32bit Cygwin,缩短输出)
__Z4inc1v:
__Z4inc2v:
lock addl $1, _counter ; both seq_cst and relaxed
ret
__Z5lock1v:
__Z5lock2v:
movl $1, %edx
L5:
movl %edx, %eax
xchgb _spinlock, %al ; both seq_cst and acquire
testb %al, %al
jne L5
rep ret
__Z7unlock1v:
movl $0, %eax
xchgb _spinlock, %al ; seq_cst
ret
__Z7unlock2v:
movb $0, _spinlock ; release
ret
x86_64bit 更新:(参见unlock1 中的mfence)
_Z4inc1v:
_Z4inc2v:
lock addl $1, counter(%rip) ; both seq_cst and relaxed
ret
_Z5lock1v:
_Z5lock2v:
movl $1, %edx
.L5:
movl %edx, %eax
xchgb spinlock(%rip), %al ; both seq_cst and acquire
testb %al, %al
jne .L5
ret
_Z7unlock1v:
movb $0, spinlock(%rip)
mfence ; seq_cst
ret
_Z7unlock2v:
movb $0, spinlock(%rip) ; release
ret
【问题讨论】:
-
如果你想知道机器码是什么,为什么不直接看一下编译结果??
-
@KerrekSB:这只是问题的一小部分。我可以看到一些栅栏和锁,但想知道它们的真正作用。
-
使用
g++ -fverbose-asm -O1 -std=c++11 -S可能会提供更易读的汇编代码...... -
@BasileStarynkevitch:谢谢,已更新
-
也许大部分实际成本是缓存同步。
标签: c++ multithreading c++11 atomic memory-fences