返回信息流看了一个单例的实现,用的内存屏障来同步
static Singleton* volatile pInstance;
Singleton* Singleton::instance
{
Singleton* tmp = pInstance;
... // insert memory barrier (1)
if ( tmp == 0 ) {
Lock lock;
tmp = pInstance;
if (tmp == 0) {
tmp = new Singleton;
... // insert memory barrier (2)
pInstance = tmp;
}
}
return tmp;
}
在多线程下,内存屏障是怎么实现“使得此屏障点之前的所有读写操作都执行后才可以开始执行此点之后的操作”?
他与volatile关键字比较??
这是一条镜像帖。来源:北邮人论坛 / cpp / #93075同步于 2016/8/15
该镜像源已超过 30 天没有更新,可能在源站已被删除。
CPP机器人发帖
[问题]关于内存屏障
shan10211865
2016/8/15镜像同步11 回复
订阅后,新回复会通过你的通知中心匿名送达。
9 条回复
如果是一个线程,那么按照程序顺序,后面一定能看到前面写的,就算不加fence也可以。
如果是多个线程,你真正想要的结果是:凡是看见pInstance不是nullptr,就一定能看到*pInstance里面的内容。这样做:
#include <atomic>
#include <mutex>
using namespace std;
static atomic<Singleton*> pInstance;
static mutex pInstanceLock;
Singleton* Singleton::instance
{
Singleton* tmp = pInstance.load(memory_order_acquire); // 这个自带fence
// 或者这样手动加fence也行:
// Singleton* tmp = pInstance.load(memory_order_relaxed); // 这个不带fence
// atomic_thread_fence(memory_order_acquire); // 这是fence。和上一行加起来相当于一个acquire的load
if ( tmp == nullptr ) {
lock_guard<mutex> guard(pInstanceLock); // 获取锁,并在离开作用域时解锁
tmp = pInstance.load(memory_order_relaxed); // lock顺序已经非常强了。
// 这里我们只是想检测pInstance是不是nullptr,而不是想看进去。
// 如果另一个线程想写pInstance,它必定要获得锁,而且在写完pInstance后必定解锁。
// 当前线程想进入这个块,也必须获得锁,只有获得了锁才能继续。
// 所以,别人的解锁操作+我的加锁操作,在一起保证了:如果别的线程
// 把pInstance改成了非nullptr值,那么当前线程一定能看到。
if (tmp == nullptr) {
tmp = new Singleton;
pInstance.store(tmp, memory_order_release); // 这个自带fence
// 或者这样手动加fence也行:
// atomic_thread_fence(memory_order_acquire); // 这是fence。和下面一行加起来相当于release的store
// pInstance.store(tmp, memory_order_relaxed); // 这个不带fence
}
} // 这里自动解锁
return tmp;
}
建议用C++11的atomic类型,而不是volatile。C/C++语言对volatile的语义定义得很含糊,具体的语义基本上都要看具体的编译器的手册了(比如GCC/G++的手册)。在C++11出现之前,各个编译器有自己的扩展,包括原子操作以及fence等。但都和具体的编译器甚至平台相关,不容易移植(真要移植是可以的,需要自己封装各个编译器的扩展)。
但atomic类型和memory order有明确的语义,编译器要保证release和acquire之间的一些可见性的。大概意思就是:如果一个acquire load读到了release store写入的值,那么这个acquire load之后所有的load都会读到这个release store之前的store写入的值。而fence的作用是:如果是一个relaxed load之后有一个acquire fence,那么这个load相当于是acquire的,这个acquire fence之后的其它load可以有acquire load之后的load一样的效果;同样,如果一个release fence之后有一个relaxed store,那么这个store相当于release的,这个release fence之前的其它store有release store之前的store一样的效果。
如果只是要用C++语言的话,不用太在意怎么实现的。如果在意的话……这就要涉及到CPU了。在x86上,acquire load就是普通的MOV指令(内存到寄存器),release store也是普通的MOV指令(寄存器到内存),而acquire fence和release fence都是“什么也不做”(编译器直接省略掉),因为x86本身的顺序已经够强了,在x86上,所有的load都是acquire,所有的store都是release。整个过程不需要涉及MFENCE之类的指令(除非使用更强的seq_cst顺序,但对于实现单例来说seq_cst没必要)。
噫~作者和读者在北邮人论坛邂逅
【 在 Wizmann 的大作中提到: 】
: 难道你看的是我的博客?。。。
: http://wizmann.tk/read-paper-barrier.html
我觉得最好的办法是做成函数里的static变量,在构造函数里new东西,执行init之类的,这样c++11可以保证多线程不出问题,也是惰性初始化,写起来还简单。。。连系统API都不需要,还可移植
额……没有诶
我那个是Scott Meyers的单例の例子,然后看到内存屏障,看功能描述似乎跟volatile关键字有异曲同工之妙,所以上来问问
有时间去你博客学习学习
【 在 Wizmann 的大作中提到: 】
: 难道你看的是我的博客?。。。
: http://wizmann.tk/read-paper-barrier.html
兄弟,跑偏了,我的那段代码是GetInstance时的实现问题
【 在 soultuanz 的大作中提到: 】
: 我觉得最好的办法是做成函数里的static变量,在构造函数里new东西,执行init之类的,这样c++11可以保证多线程不出问题,也是惰性初始化,写起来还简单。。。连系统API都不需要,还可移植
atomic跟volatile不冲突的吧,一个解决原子类型,另一个解决多线程下编译器优化带来的副作用
C11的atomic的底层实现好像也是用到了volatile……
【 在 nuanyangyang 的大作中提到: 】
: 如果是一个线程,那么按照程序顺序,后面一定能看到前面写的,就算不加fence也可以。
: 如果是多个线程,你真正想要的结果是:凡是看见pInstance不是nullptr,就一定能看到*pInstance里面的内容。这样做:
: [code=c]
: ...................
【 在 shan10211865 的大作中提到: 】
: atomic跟volatile不冲突的吧,一个解决原子类型,另一个解决多线程下编译器优化带来的副作用
不是这么说。atomic解决了原子访问,而解决读写顺序的是memory order,也就是load和store的那个参数,比如memory_order_acquire
说“编译器优化带来的副作用”不科学。因为这种“非sequentially consistent的行为”不一定是编译器带来的。
CPU本身就不保证普通的load/store有sequential consistency(如果要保证的话,代价太大了)。要想作此保证,汇编程序员必须按照CPU的指令的语义,写正确的指令序列(x86一般不用做什么,PowerPC/ARM什么的一般需要fence)。
而编译器的工作永远是在做变换,我用“变换”这个词,刻意避免“优化”,因为不管优化不优化,都是把C++的源代码翻译成汇编代码(或者机器码)。C++语言是人设计的、可移植的高级语言。高级语言有语义。比如C++11的weak ordering内存模型在有acquire/release/seq_cst标注的情况下,是要保证在C++变量的层次上的一定的可见性的(比如两个store(release)一定不能互换顺序)。所以编译器要把这两个store(release)编译成恰当的机器指令,使得CPU执行的时候,这两个内存访问请求不会互换顺序,而不仅仅是不调换两个指令的顺序。
举个栗子:
atomic<int> a(0), b(0);
void thread1() {
a.store(1, memory_order_seq_cst);
int bb = b.load(memory_order_seq_cst);
printf("%d\n", bb);
}
void thread2() {
b.store(2, memory_order_seq_cst);
int aa = a.load(memory_order_seq_cst);
printf("%d\n", aa);
}
然后编译器老老实实地,“没有优化地”把它编译成了下面这样:
thread1:
mov [a], 1
mov eax,
// print eax here
thread2:
mov [b], 2
mov eax, [a]
// print eax here
结果线程1看到b==0,同时线程2看到a==0(上述C++程序在什么机器上都不应该看到这个结果,但上述x86汇编是[b]可以看到这种结果的)。编译器喊冤:冤枉啊!我没有优化啊!我没有交换这两个指令的顺序啊!
显然编译器没有优化:它没有去“捣乱”去交换指令的顺序,但编译器的翻译正确吗?不正确。C++11里,seq_cst顺序的读和写,即使没有手动加fence,也是不能交换顺序的。但x86里,如果先写后读(W->R),但这个写和这个读的地址不一样,那么CPU是可以交换这两个内存访问的顺序的。所以,这是编译器的错:它不应该把store简简单单地翻译成一个MOV指令。
正确的翻译方法是:
- load(seq_cst)翻译成普通的MOV %reg, [mem]
- store(seq_cst)翻译成两条指令的序列:MOV [mem], %reg; MFENCE
这样,上述程序会翻译成:
thread1:
// store(seq_cst)
mov [a], 1
MFENCE
// load(seq_cst)
mov eax,
// print eax here
thread2:
// store(seq_cst)
mov [b], 2
MFENCE
// load(seq_cst)
mov eax, [a]
// print eax here
在x86上执行的时候,那个MFENCE会阻止后面的load跑到前面的store前面去。所以,也就看不到a==0&&b==0了。这个MFENCE不能省略。
所以,正确的说法是“编译器应该把C++程序翻译成能够[b]正确表达C++语义的汇编序列”,而不仅仅是“不做优化”。
: C11的atomic的底层实现好像也是用到了volatile……
这个也不一定。还是那句话:C/C++对volatile修饰符的定义很模糊。所以说不上“用volatile来实现”。正确的说法还是“用正确的汇编序列来实现”。一般来说编译器对atomic类型的方法有特别的认识(即:intrinsic function),编译器可以直接利用规则编译比如atomic::load这个方法。比如“在x86上,load(memory_order_acquire)翻译成MOV指令”,“在PowerPC上load(memory_order_acquire)翻译成ld; cmp; bc; isync指令序列”。
我说的优化并不是单指指语序执行的顺序问题,而是指多线程下对某个变量可能会有各自的"副本"的问题,在这个例子里面,a 与b 同时为0的话,已经违背了volatile的原则了吧?
对于std::atomic模板,直接用的话只支持那几种基本数据类型,自定义的类要用于std::atomic类型,必须要实现模板具备的操作,也是一件挺麻烦的事情,就拿个单例CSingledog的来说,明明实际就一个GetInstance的对外的实用接口,可是如果要使CSingledog能应用于atomic类型,那么CSingledog还必须实现is_lock_free()、store()、load()……不得不说太麻烦了
atomic的实现用到了volatile,是说atomic需要volatile的声明,当然也需要Memory Order
参考[http]http://www.cnblogs.com/haippy/archive/2013/09/05/3301408.html
【 在 nuanyangyang 的大作中提到: 】
:
: 不是这么说。atomic解决了原子访问,而解决读写顺序的是memory order,也就是load和store的那个参数,比如memory_order_acquire
: 说“编译器优化带来的副作用”不科学。因为这种“非sequentially consistent的行为”不一定是编译器带来的。
: ...................