C++ 0x: 内存模型

自C++11有了多线程,天然 原子类型(atomic)也是少不了的.ios

提到原子类型必然是与内存模型(std::memory_order)相互关联的.其实半年前就有接触到,半年的时间里对它的理解仍是只知其一;不知其二,并且一直没有时间深刻的看看,正好得了肩周炎,就躺着看了看,又查查了在此也作一个总结.c++

C++标准库提供一下几种memory_order:程序员

enum memory_order {缓存

    memory_order_relaxed,
    memory_order_consume,
    memory_order_acquire,
    memory_order_release,
    memory_order_acq_rel,
    memory_order_seq_cst
多线程

};app

 

在正式进入正题以前还有几个概念是咱们必需要了解的:ide

Sequence before: 在同一线程内 evaluation A操做可能先于 evaluation B操做遵循evaluation order.优化

Carries dependency: 在同一线程内 evaluation A 先于 evaluation B执行,若是evaluation B操做须要evaluation A的值那么咱们就说 evaluation B 依赖 evaluation A操做的值.ui

若是 evaluation B 依赖 evaluation A操做的值须要知足如下条件:this

  1) evaluation A操做的值做为evaluation B的操做数,除了如下几种状况

         a)evaluation B调用了std::kill_dependency

         b)evaluation A操做的值做为 &&, ||, :? 和 , 运算符的左操做数.

  2) evaluation A操做的值写入到一个变量(variable)M中, evaluation B从M中读取该值.

  3) 假设有 evaluation A操做依赖 evaluation X操做的值,而evaluation B操做依赖evaluation A操做的值那么此时evaluation B依赖evaluation X.

 

(1),在了解内存模型以前咱们还须要了解:

1,因为技术的革新,咱们在运行咱们写好的代码的时候,好比读取变量的值,并不必定是从内存中读取的,因为为了快速读取变量的值, 因此该值的一个备份(在第一次从内存中读取该值的时候)可能会被放在CPU高速缓存/寄存器(固然这也不是说内存中就没有值了,只是由于从CPU高速缓存/寄存器中读取更快!内存中依然有一份值).

CPU高速缓存:http://baike.baidu.com/link?url=uOY6wdHUpoeMQ0NWyo2957fXIdljtBThvVnGNtwX1nDSJZ3TsglHdDQuKsj_oKMReUTqXHO3v5DxOOozI1iTHK

CPU高速缓存好文章一篇(这一篇最好读读前几节):http://blog.csdn.net/robertsong2004/article/details/38340247

2,还有不得不说的是(乱序,见下面补充):咱们的代码并不必定按照彻底按照咱们写的方式来运行(固然了确定是有办法来限制编译器优化形成的适当的乱序的具体参阅这里:https://my.oschina.net/u/2516597/blog/676927).

 

(2),关于乱序:

1,编译器出于优化目的,在编译阶段对代码进行适当重排序.

2,程序执行期间,CPU乱序执行指令流.

3, inherent cache 的分层及刷新策略使得有时候某些写读操做的从效果上看,顺序被重排 。(这个我也不懂)

以上乱序现象虽然来源不一样,但从源码的角度,对上层应用程序来讲,他们的效果其实相同:写出来的代码与最后被执行的代码是不一致的。这个事实可能会让人很惊讶:有这样严重的问题,还怎么写得出正确的代码?这担心是多余的了,乱序的现象虽然广泛存在,但它们都有很重要的一个共同点:在单线程执行的状况下,乱序执行与不乱序执行,最后都会得出相同的结果 (both end up with the same observable result), 这是乱序被容许出现所须要遵循的首要原则,也是为何乱序虽然一直存在但却多数程序员大部分时间都感受不到的根本缘由.

 

(3),接着咱们须要明白的另一个概念是:

上面也说了,单线程下面感受不到,也没啥影响,那么多线程就问题来了!

1,即便是普通的变量咱们对它的读写也不是atomic的,也就是说咱们在对它读的时候也能对它写,可是获得的结果多是以前的旧值,也多是新写入的值,也多是个写了一半的值.

2,CPU高速缓存带来的一个问题,若是你看了上面的百度百科以及那篇博客关于CPU高速缓存的介绍,我想您应该明白,咱们对于变量的读写都是在高速缓存中完成的!也就是说咱们并无实际修改到内存中的数据!!问题可就大了呀,多线程下一个线程修改了数据!另一个线程竟然没看到新值!(仅仅看到了内存中的旧值,此时新的值还没被同步到内存里面)。咱们能够理解为,把高速缓存中的值往内存中同步(牢记这个词后面N屡次用到)的不及时.另外在一般状况下 什么时候同步同步 是两个不一样的概念(这里牢记!!!!)

当上面这两个问题交织在一块儿的时候想一想有多可怕,简直是个黑洞! 那么咱们就须要经过内存模型(memory_order)来限制这些了!

 

(4),std::memory_order_relaxed(自由序列)

该模式下仅仅保证了读写的完整性且还有要求是单个线程内的同一个原子类型的全部操做不能进行乱序.也就是说对一个多线程下的变量进行读写的时候,保证必定会读到完整的值(不会出现读到一个不完整的值,至于该值是新值仍是旧值,是无法保证的,同步几回 和 什么时候同步 CPU说了算),写的时候仅仅保证值必定会被完整的写入(寄存器),至于什么时候同步,以及对是否在每次写入结束后当即同步到内存中就要看CPU了.

demo1 for std::memory_order_relaxed

#include <atomic>
#include <thread>
#include <cassert>

std::atomic<bool> x,y;
std::atomic<int> z;

void write_x_then_y()
{
  x.store(true,std::memory_order_relaxed);  // 1
  y.store(true,std::memory_order_relaxed);  // 2
}
void read_y_then_x()
{
  while(!y.load(std::memory_order_relaxed));  // 3
  if(x.load(std::memory_order_relaxed))  // 4
    ++z;
}
int main()
{
  x=false; //5
  y=false; //6
  z=0; //7
  std::thread a(write_x_then_y); //A
  std::thread b(read_y_then_x);  //B
  a.join();
  b.join();
  assert(z.load()!=0);  // 8

  return 0;
}

解析demo1:

assert是仍然有可能触发的! 首先咱们须要注意到的是不管是read操做(load),仍是write(store),咱们都指定内存模型为std::memory_order_relaxed,这样一来也就意味着1和2处的操做有可能被乱序执行5,6和7处也同样可能被乱序执行。不只仅是乱序这么简单1, 2, 5, 6, 7处的写操做极可能都仅仅写入了高速缓存/寄存器,也可能并无同步内存中的,那么在3和4处执行的read操做就可能即便y load到了true, 而x仍然load到false.

 

demo2 for std::memory_order_relaxed

// Thread 1:
r1 = y.load(memory_order_relaxed); // A
x.store(r1, memory_order_relaxed); // B


// Thread 2:
r2 = x.load(memory_order_relaxed); // C 
y.store(42, memory_order_relaxed); // D

x 和 y都默认初始化为0.

demo2解析:

颇有可能 r1 = r2 = 42,由于尽管在线程1中A sequece-before B,线程2中 C sequence-before D.可是没有什么保证D必定是在A以后运行, 一样也没法保证B必定在C以前运行.尽管如此D产生的side-effect仍是对于A可见的同理B产生的side-effect也是对于C可见的.

 

(4.5), std::memory_order_release

该memory_order用于atomic store operation且保证全部发生在该operation操做以前的读写操做都不能被乱序(reordered).

 

(5), std::memory_order_release 和 std::memeory_order_acquire(注意这两个必定是成对出现的).

该模型下不只仅提供了保证读写的完整性,保证同一个线程内的同一个原子变量的全部操做不能乱序,并且提供了同步(这个同步可厉害了见下文)!

若是有一个atomic变量,在线程A中atomic store(taged std::memory_order_release)一个值到该变量中去,在线程B中atomic load(taged std::memory_order_acquire)atomic 变量中的值,当atomic 变量的load(taged std::memory_order_acquire)操做完成后,全部发生在线程A中atomic store(taged std::memory_order_release)操做以前的其余变量(普通变量以及store taged std::memory_order_relaxed)写入的值(产生的side effect)对于线程B中atomic 变量load(taged std::memory_order_acquire)以后的操做都可见(能够把线程B中atomic 变量load taged std::memory_order_acquire理解为一个同步点可是这个同步点不只仅同步了该atomic变量也同步了其余变量(其余变量包括普通变量以及taged std::memory_order_relaxed的atomic变量)!

好比:

线程 A 原子性地把值写入 x (release), 而后线程 B 原子性地读取 x 的值(acquire). 这样线程 B 尽量的读取到 x 的最新值。注意 release - acquire 有个牛逼的反作用:线程 A 中全部发生在 release x 以前的写操做,对在线程 B acquire x 以后的任何读操做均可见!原本 A, B 间读写操做顺序不定。这么一同步,在 x 这个点先后, A, B 线程之间有了个顺序关系,称做: inter-thread happens-before.
 

demo1 for std::memory_order_release/acquire

#include <atomic>
#include <thread>
#include <assert.h>

std::atomic<bool> x,y;
std::atomic<int> z;
void write_x()
{
  x.store(true,std::memory_order_release);
}
void write_y()
{
  y.store(true,std::memory_order_release);
}
void read_x_then_y()
{
  while(!x.load(std::memory_order_acquire)); //1
  if(y.load(std::memory_order_acquire))  //2
    ++z;
}
void read_y_then_x()
{
  while(!y.load(std::memory_order_acquire)); //3
  if(x.load(std::memory_order_acquire))    //4
    ++z;
}
int main()
{
  x=false;
  y=false;
  z=0;
  std::thread a(write_x); //A线程
  std::thread b(write_y); //B线程
  std::thread c(read_x_then_y); //C线程
  std::thread d(read_y_then_x); //D线程
  a.join();
  b.join();
  c.join();
  d.join();
  assert(z.load()!=0); // 5

  return 0;
}

上面的demo中:

assert仍然有可能触发.

上面的例子中 x y 都是由不一样的线程写入的,所以在 线程C和D 中:1和3 load(acqiire)操做因为是位于while中所以能够屡次进行同步(若是一次不成功就再同步一次直到load出来的值为true为止). 可是3和4的load(acquire)操做可能会loadfalse,由于 x.store和y.store 操做是做为 两个线程(A和B) 来执行的,因此在线程C中 1处的操做只能看到 线程A(线程A必须早于线程C时)中的 store(release) 的值,至于 线程C 2处的操做 可能因为CPU的乱序执行 形成运行到了 2处线程B 却还没执行(或者刚刚开始执行,或者执行一半)这样一来看到就仍是false; 线程D中同理.

 

demo2 for std::memory_order_release/acquire

#include <iostream>
#include <atomic>
#include <thread>
#include <cassert>

#include "arg.h"

std::atomic<bool> a{ false };
std::atomic<bool> b{ false };
std::atomic<int> c{ 0 };

void writeA()noexcept
{
	a.store(true, std::memory_order_release);//1
}

void writeB()noexcept
{
	b.store(true, std::memory_order_release); //2
}

void setA()noexcept
{
	a.store(false, std::memory_order_release);//3
}

void readAB()noexcept
{
	while (a.load(std::memory_order_acquire)); //4

	if (b.load(std::memory_order_acquire)) { //5
		++c;
	}
}



int main()
{
	a = false;
	b = false;
	c = 0;

	std::thread tOne{ writeA };//A线程
	std::thread tTwo{ writeB };//B线程
	std::thread thread{ setA };//C线程
	std::thread tThree{ readAB };//D线程

	tOne.join();
	tTwo.join();
	tThree.join();
	thread.join();

	assert(c.load() != 0);

	return 0;
}

这个demo和上面的demo有稍许不一样:

assert还有可能触发的!上面的demo中有两个线程 A和Ca 进行 store(release) 操做,可是在 线程D 中只有 4处 的操做对 a 进行了 load(acquire) 操做,若是 线程A和C 是先于 线程D 执行的 那么在 load(acquire) 的时候就能看到线程 A和C store(release) 的值并进行同步至于哪一个先同步顺序是不肯定的,可能同步一次就成功了,可能要几回(这样一来就至关于线程间有了个简单秩序).

 

demo3 for std::memory_order_release/acquire

#include <atomic>
#include <thread>
#include <cassert>

std::atomic<bool> x,y;
std::atomic<int> z;

void write_x_then_y()
{
  x.store(true,std::memory_order_relaxed);  // 1 自旋,等待y被设置为true
  y.store(true,std::memory_order_release);  // 2
}
void read_y_then_x()
{
  while(!y.load(std::memory_order_acquire));  // 3
  if(x.load(std::memory_order_relaxed))  // 4
    ++z;
}
int main()
{
  x=false;
  y=false;
  z=0;
  std::thread a(write_x_then_y); //A线程
  std::thread b(read_y_then_x);  //B线程
  a.join();
  b.join();
  assert(z.load()!=0);  // 5

  return 0;
}

这个demo又与上面两个稍许不一样:

assert是会被触发的! 咱们注意到 1和2操做 是在同一个线程中分别对 x和y 进行 store(release)。且一般状况下 1操做 是先于 操做2 的,并且 线程A 是先于 线程B 的(即便 线程A 不是先于 线程B 的因为在 3操做 处有个while直到同步到 y 的值为true为止, 既然 y load(acquire)到了true 那么多半状况下 线程A 也差很少执行完成了),这样一来在 3操做y.load(acquire) 线程B 就能看到 全部在 该 load(acquire) 操做以前的 store(release) 操做的值,无论是否是同一个原子类型(也就是说其实也至关于对x也同步了一次)!

 

(6), std::memory_order_release/cousume(实际上是:release/acquire的一种只是反作用小了点)

在进行了解以前咱们须要了解:

Denpendency-ordered before(前序依赖):

前序依赖 是针对线程之间的!符合如下2点:

1, 线程A 原子变量M 执行release操做, 线程B 原子变量M 运行consume操做读取 线程A 原子变量M 进行release操做存储的值.这个时候就是 B线程 前序依赖 A线程.

2,若是 Y前序依赖 X, Z前序依赖Y, 那么 Z前序依赖X. 也就是Y携带依赖.

 

假设有一个atomic变量M,在线程A中对M atomic store(taged std::memory_order_release),在线程B中对M atomic load(taged std::memory_order_consume)里面存储的值.全部发生在线程A 对M atomic store操做以前的变量(普通变量或者taged std::memory_order_relaxed的atomic变量)的写入的值,若是M 在线程A中的atomic store(taged std::memory_order_release)依赖这些变量(普通变量或者taged std::memory_order_relaxed的atomic变量)的值,那么线程B中M的atomic load(taged std::memory_order_consume)以后的操做都能看到M在线程A中atomic store(taged std::memory_order_release)依赖的这些变量的值(其实就是 数据依赖 和 携带依赖).

 

如今在release/consume模型中: 同步仍是同样的在每次对 原子类型(atomic)进行 load taged consume操做的时候进行 同步,这回反作用弱了点:能够理解为只同步该原子变量自己以及该原子变量store(taged release)时候依赖的那些变量的值.

例如:

假设有一个atomic variable M, 在线程AM.store(val, release), 在线程B M.load(consume), 可是在线程A中M.store(val, release) 以前 val 的值须要 依赖其余变量的值 这个时候咱们就说 M.store(release)前序依赖val, 因为 val 依赖其余变量 所以 val携带依赖,所以M.store也依赖其余变量, 所以在线程B M.load(consume) 前序依赖 线程A 中的 M.store(val, release)操做且对 val 以及其余变量也有 数据依赖前序依赖,也就是说 线程A 中的 M.stroe(val, release)携带依赖!

 

demo for std::memory_order_release/consume

struct X
{
int i;
std::string s;
};

std::atomic<X*> p;
std::atomic<int> a;

void create_x()
{
  X* x=new X;
  x->i=42; //x
  x->s="hello";//y
  a.store(99,std::memory_order_relaxed);  // 1
  p.store(x,std::memory_order_release);  // 2
}

void use_x()
{
  X* x;
  while(!(x=p.load(std::memory_order_consume)))  // 3
    std::this_thread::sleep(std::chrono::microseconds(1));
  assert(x->i==42);  // 4
  assert(x->s=="hello");  // 5
  assert(a.load(std::memory_order_relaxed)==99);  // 6
}

int main()
{
  std::thread t1(create_x); //A线程
  std::thread t2(use_x); //B线程
  t1.join();
  t2.join();

  return 0;
}


上面的demo中:

4和5 是不会触发的,可是 6有可能触发!

尽管 1操做 是位于 2操做 以前的 因为 2操做 存储时为relaxed仅仅保证读写的完整性同步以及什么时候同步,并不禁代码控制。 可是 1和2 的存储操做用的是release的内存模型也就是 赖A(线程)写,而在 线程B 因为须要读取p的数据也就是 赖B(线程)读, 那么 线程B 同步(load(consume))后就能看到 线程A p写入的值.

总结就是: 线程B 前序依赖 线程A, 操做4和5数据依赖x和y.

固然仍是有办法来打破依赖关系的:

std::kill_dependency();

它的实现很简单很简单:

template<class _Ty>
    _Ty kill_dependency(_Ty _Arg) noexcept
    {    // magic template that kills dependency ordering when called
    return (_Arg);
    }

就只是简单的拷贝,告诉程序读取值的时候从寄存器/高速缓存读取.

 

若是提供了atomic而没有提供fence把fencen想象成一个指定的同步点,其实也就是)就不算一个完整的atomic接下来咱们了解一下c++标准库提供的fence:

Fence-atomic synchronization:

假设有原子变量: std::atomic<int> val;

一个 std::atomic_thread_fence(release) 在线程A 中经过 线程B 中的 val.load(acquire)进行同步 须要知足如下:

①, 有一个val.store()操做(注意能够以任意memory_order指定该操做).

②, 线程B中的val.load(acquire)操做读取①中store的值(或者这个值将被写入在①的操做以后, 但此时1的store操做为release的!)

③, std::atomic_thread_fence(release)位于①操做以前.

当执行到 线程B val.load(acquire)操做的时候,会同步发生在 std::atomic_thread_fence(release)以前全部非原子类型和relaxed模式下的原子写操做, 可是位于std::atomic_thread_fence(release)和val.store()之间非原子类型和relaxed模式下的原子写操做并不会被同步。

demo for Fence-atomic synchronization:

#include <iostream>
#include <thread>
#include <atomic>
#include <cassert>

std::atomic<bool> bAtom{ false };
bool bValue{ false };
int iValue{ 0 };

void writeValue()
{
	bValue = true; //1
	std::atomic_thread_fence(std::memory_order_release); //2

	iValue = 1; //3
	bAtom.store(true, std::memory_order_release);//4
}

void readValue()
{
	while (!bAtom.load(std::memory_order_acquire)); //5

	assert(bValue);  //不可能触发!  6
	assert(iValue == 1); //可能触发! 7
}

int main()
{
	std::thread tOne{ writeValue }; //线程A
	std::thread tTwo{ readValue }; //线程B

	tOne.join();
	tTwo.join();

	return 0;
}

 

Atomic-fence synchronization:

假设有原子变量: std::atomic<int> val;

一个 线程A 中的 val.store(release) 操做被同步经过 线程B 中的 std::atomic_thread_fence(acquire) 须要知足如下:

①, 有一个 val.load() 的读操做(可使任意memory_order).

②, 经过读取 线程A val.store(release) 存储的值

③, 其中操做要先于 线程B 中的 std::atomic_thread_fence(acquire)

总结只要是发生在 线程A val.store(release) 操做以前 非原子操做relaxed的原子操做都会在 线程B中的std::atomic_thread_fence(acquire)处被同步.

#include <iostream>
#include <thread>
#include <atomic>
#include <cassert>

std::atomic<bool> bAtom{ false };
std::atomic<bool> bOne{ false };
std::atomic<bool> bTwo{ false };
bool bValue{ false };
int iValue{ 0 };

void writeValue()  //thread A
{
	bValue = true;
	bOne.store(true, std::memory_order_relaxed);
	bAtom.store(true, std::memory_order_release);

	bTwo.store(true, std::memory_order_relaxed);
	iValue = 1; 
}

void readValue() //thread B
{
	while (!bAtom.load(std::memory_order_consume)); //固然这里也能够是: acquire.
	std::atomic_thread_fence(std::memory_order_acquire);
	assert(bValue);  //不可能触发! 

	assert(iValue == 1); //可能触发! 
	assert(bOne.load(std::memory_order_relaxed)); //可能触发!
	assert(bTwo.load(std::memory_order_relaxed)); //可能触发!
}

int main()
{
	std::thread tOne{ writeValue }; //线程A
	std::thread tTwo{ readValue }; //线程B

	tOne.join();
	tTwo.join();

	return 0;
}

 

Fence-fence synchronization:

在 线程A 中有 std::atomic_thread_fence(release) 经过 线程B 中的 std::atomic_thread_fence(acquire) 同步 须要知足如下:

①,  有一个原子变量: std::atomic<int> val;

②, 在 线程A 中调用 val.store()(能够是任何memory_order).

③, 线程A 中的 std::atomic_thread_fence(release)必定是在 val.store()以前的.

④, 有一个 val.load()(能够是任何memory_order)在线程B中.

⑤, 经过 B线程中的val.load() 读取 A线程中的val.store()写入的值(或者读取一个在 ② 操做以前一个原子类型经过release写入的值).

⑥, 其中⑤操做必定要发生在 线程B的std::atomic_thread_fence(acquire)操做以前.

总结全部比线程B中std::atomic_thread_fence先发生,非原子读写以及原子relaxed的读写都会被同步.

 

demo for Fence-fence-synchronization

include <iostream>
#include <thread>
#include <atomic>
#include <cassert>
#include <initializer_list>

std::atomic<bool> bAtom{ false };
std::atomic<bool> bOne{ false };
std::atomic<bool> bTwo{ false };
bool bValue{ false };
int iValue{ 0 };

void writeValue()  //thread A
{
	bValue = true;
	bOne.store(true, std::memory_order_relaxed);
	
	std::atomic_thread_fence(std::memory_order_release);
	bAtom.store(true, std::memory_order_release);

	bTwo.store(true, std::memory_order_relaxed);
	iValue = 1; 
}

void readValue() //thread B
{
	while (!bAtom.load(std::memory_order_consume)); //固然这里也能够是: acquire.
	std::atomic_thread_fence(std::memory_order_acquire);

	assert(bValue);  //不可能触发! 
	assert(bOne.load(std::memory_order_relaxed)); //不可能触发!

	assert(iValue == 1); //可能触发! 
	assert(bTwo.load(std::memory_order_relaxed)); //可能触发!
}

int main()
{
	std::thread tOne{ writeValue }; //线程A
	std::thread tTwo{ readValue }; //线程B

	tOne.join();
	tTwo.join();

	return 0;
}
相关文章
相关标签/搜索