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;
}
相關文章
相關標籤/搜索