__declspec(selectany) 支持h文件定义全局变量,重复包含不报错
int i = 2; //变量i还是不用加volatile修饰
#define ACCESS_ONCE(x) (*(volatile typeof(x) *)&(x))
#define READ_ONCE(x) ACCESS_ONCE(x)
#define WRITE_ONCE(x, val) ({ ACCESS_ONCE(x) = (val); })
a = READ_ONCE(i);
WRITE_ONCE(i, 2);
volatile仅仅是保证int的地址对齐,而对齐后的×××在现代处理器中,是能够做到原子性读写的。
在C++中volatile具有以下特性:
- 易变性:
所谓的易变性,在汇编层面反映出来,就是两条语句,
下一条语句不会直接使用上一条语句对应的volatile变量的寄存器内容,而是重新从内存中读取。 - "不可优化"性:
volatile告诉编译器,不要对我这个变量进行各种激进的优化,甚至将变量直接消除,保证程序员写在代码中的指令,一定会被执行。 - “顺序性”:
能够保证Volatile变量间的顺序性,编译器不会进行乱序优化。Volatile变量与非Volatile变量的顺序,编译器不保证顺序,可能会进行乱序优化。
//CAS作为最基础的RMW操作,其他所有RMW操作都可以通过CAS来实现
bool CAS( int * pAddr, int nExpected, int nNew )
atomically {
if ( *pAddr == nExpected ) {
*pAddr = nNew ;
return true ;
}
else
return false ;
}
//CAS变种
int CAS( int * pAddr, int nExpected, int nNew )
atomically {
if ( *pAddr == nExpected ) {
*pAddr = nNew ;
return nExpected ;
}
else
return *pAddr;
}
//例如 fetch-and-add(FAA)
int FAA( int * pAddr, int nIncr )
{
int ncur = *pAddr;
do {} while ( !compare_exchange( pAddr, ncur, ncur + nIncr ) ;//compare_exchange失败会返回当前值于ncur
return ncur ;
}
在C++11的原子lib中,主要有以下RMW操作:
std::atomic<>::fetch_sub()
std::atomic<>::fetch_and()
std::atomic<>::fetch_or()
std::atomic<>::fetch_xor()
std::atomic<>::exchange()
std::atomic<>::compare_exchange_strong()
std::atomic<>::compare_exchange_weak()
//实现自己需要的原子RMW操作,我们需要一个原子对内存中值执行乘法,也就是 atomic fetch_multiply
uint32_t fetch_multiply(std::atomic<uint32_t>& shared, uint32_t multiplier)
{
uint32_t oldValue = shared.load();
while (!shared.compare_exchange_weak(oldValue, oldValue * multiplier))
{
}
return oldValue;
}
//对结构体进行原子修改
struct Terms
{
uint32_t x;
uint32_t y;
};
std::atomic terms;
void atomicFibonacciStep()
{
Terms oldTerms = terms.load();
Terms newTerms;
do
{
newTerms.x = oldTerms.y;
newTerms.y = oldTerms.x + oldTerms.y;
}
while (!terms.compare_exchange_weak(oldTerms, newTerms));
}
//是不是在内部加了锁
std::atomic<> template包含了一个is_lock_free()成员来用于判断该原子类型是原子操作是否是lock-free的。
—同时满足以下条件的原子类的原子操作才能做出是lock-free的保证
- The compiler is a recent version MSVC, GCC or Clang.
- The target processor is x86, x64 or ARMv7 (and possibly others).
- The atomic type is std::atomic<uint32_t>, std::atomic<uint64_t> or std::atomic<T*> for some type T.
我们必须加上编译器的barrier来防止编译器的乱序优化:
#define COMPILER_BARRIER() asm volatile(“” ::: “memory”)
int Value;
int IsPublished = 0;
void sendValue(int x)
{
Value = x;
COMPILER_BARRIER(); // prevent reordering of stores
IsPublished = 1;
}
int tryRecvValue()
{
if (IsPublished)
{
COMPILER_BARRIER(); // prevent reordering of loads
return Value;
}
return -1; // or some other value to mean not yet received
}
//cache同步
Write Invalidate(置无效):当一个CPU Core修改了一份数据X,那么它需要通知其他core将他们的cache中的X设置为无效(invalid)
Write Update(写更新):当一个CPU Core修改了一份数据X,那么它需要通知其他core将他们的cache中的X更新到最新值(如果cache中有的话)
//cache间MESI协议就包含了描述共享的状态
M(Modified): cache line数据有效,但是数据被修改过了,本Cache中的数据是最新的,内存的数据是老的,需要在适当时候将Cache数据写回内存。该cache将数据及其控制权传递到其他cache中,或者cache需要负责将数据写回到memory中,而这些操作都需要在reuse该cache line之前完成。
E(Exclusive):cache line数据有效,并且cache和memory中的数据是一致的,同时数据只在本cache中有效。直接reuse该cacheline(将cacheine中的数据丢弃,用作他用)
S(Shared):cache line的数据有效,并且cache和memory中的数据是一致的,同时该数据在多个cpu cache中也是有效的。直接reuse该cacheline(将cacheine中的数据丢弃,用作他用)
I(Invalid):本cache line的数据已经是无效的。处于invalid状态的cacheline是空的,没有数据。当新的数据要进入cache的时候,优选状态是invalid的cacheline,之所以如此是因为如果选中其他状态的cacheline,则说明需要替换cacheline数据,而未来如果再次访问这个被替换掉的cacheline数据的时候将遇到开销非常大的cache miss。
//在MESI协议中,每个CPU都会监听总线(bus)上的其他CPU对每个Cache line的所有操作,因此该协议也称为监听(snoop)协议
通常情况下,CPU需要以下几个通信message即可:
Read消息:read message用来获取指定物理地址上的cacheline数据
Read Response消息:该消息携带了read message请求的数据。read response可能来自memory,也可能来自其他的cache。例如:如果一个cache有read message请求的数据并且该cacheline的状态是modified,那么该cache必须以read response回应这个read message,因为该cache中保存了最新的数据。
Invalidate消息:该命令用来将其他cpu cache中的数据设定为无效。该命令携带物理地址的参数,其他CPU cache在收到该命令后,必须进行匹配,发现自己的cacheline中有该物理地址的数据,那么就将其移除并用Invalidate Acknowledge回应。
Invalidate Acknowledge消息: 收到invalidate message的cpu cache,在移除了其cache line中的特定数据之后,必须发送invalidate acknowledge消息。
Read Invalidate消息: 该message中也包括了物理地址这个参数,以便说明其想要读取哪一个cacheline数据。此外,该message还同时有invalidate message的功效,即其他的cache在收到该命令后,移除自己cacheline中的数据。因此,Read Invalidate message实际上就是read+invalidate。发送Read Invalidate之后,cache期望收到一个read response以及多个invalidate acknowledge。
Writeback消息: 该message包括两个参数,一个是地址,另外一个是写回的数据。该消息用在modified状态的cacheline被驱逐出境(给其他数据腾出地方)的时候发出,该命名用来将最新的数据写回到memory(或者其他的CPU cache中)
##Store Buffer
在CPU和cache之间增加store buffer这个HW block。
那么cpu 0无需等待其他CPU的相应,只需要将要修改的内容放入store buffer,然后继续执行就OK了。当cache line完成了bus transaction,并更新了cache line的状态后,要修改的数据将从store buffer进入cache line。
这种设计叫做store forwarding,当CPU执行load操作的时候,不但要看cache,还有看store buffer是否有内容,如果store buffer有该数据,那么就采用store buffer中的值。store forwarding解决了CPU 0的cache line和store buffer间的数据一致性问题。
smp_mb内存屏障。保证多核CPU的store buffer内容一致,保证store buffer完全写入cache line之前,其他数据不能先更新cache line。
###为了保证MESI协议的正确性,CPU在需要发出某个变量的a的MESI协议消息的时候,需要检查invalidate queue中是否有该变量a的invalidate消息,如果有需要先出来完成这个invliadte消息后,才能发出正确的MESI协议消息。
#无论哪一种cpu都遵守下面的规则:
[1]、从CPU自己的视角看,它自己的memory order是服从program order的
[2]、从包含所有cpu的sharebility domain的角度看,所有cpu对一个共享变量的访问应该服从若干个全局存储顺序
[3]、memory barrier需要成对使用
[4]、memory barrier的操作是构建互斥锁原语的基石