C++ 內存模型

C++ std::atomic 原子類型

原子操做:一個不可分割的操做。
標準原子類型能夠在 頭文件之中找到,在這種類型上的全部操做都是原子的。它們都有一個 is_lock_free()的成員函數,讓用戶決定在給定類型上的操做是否用原子指令完成。惟一不提供 is_lock_free()成員函數的類型是 std::atomic_flag,在此類型上的操做要求是無鎖的。能夠利用 std::atomic_flag實現一個簡單的鎖。 ios

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


class spinlock_mutex
{
  public:
    spinlock_mutex() : flag_(ATOMIC_FLAG_INIT) { }

    void lock()
    {
      while(flag_.test_and_set(std::memory_order_acquire)) ;
    }

    void unlock()
    {
      flag_.clear(std::memory_order_release);
    }

  private:
    std::atomic_flag flag_;
};

int value = 0;
spinlock_mutex mutex;

void test_function()
{
  for(int i = 0; i < 100000; i++)
  {
    std::unique_lock<spinlock_mutex> lock(mutex);
    ++ value;
  }
}

int main()
{
  std::thread t1(test_function);
  std::thread t2(test_function);
  t1.join();
  t2.join();

  assert(value == 200000);

  return 0;
}

C++ 11中的內存模型都是圍繞std::atomic展開的,下面依次介紹C++ 11中引入的內存順序。
參考: Memory Model編程

順序一致順序

默認的的順序被命名爲順序一致,由於這意味着程序的行爲和一個簡單的世界觀是一致的。若是全部原子類型實例上的操做是順序一致的,多線程的行爲就好像是全部這些操做由單個線程以某種特定的順序進行執行的同樣。
在一個帶有多處理器的弱順序的機器上,它可能致使顯著的性能懲罰,由於操做的總體順序必須與處理器之間保持一致,可能須要處理器之間進行密集的同步操做。多線程

#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_seq_cst);
}

void write_y()
{
  y.store(true, std::memory_order_seq_cst);
}

void read_x_then_y()
{
  while(!x.load(std::memory_order_seq_cst)) ;
  if(y.load(std::memory_order_seq_cst))
  {
    printf("x,y\n");
    ++ z;
  }
}

void read_y_then_x()
{
  while(!y.load(std::memory_order_seq_cst)) ;
  if(x.load(std::memory_order_seq_cst))
  {
    printf("y,x\n");
    ++ z;
  }
}

int main()
{
  x = false;
  y = false;
  z = 0;
  std::thread a(write_x);
  std::thread b(write_y);
  std::thread c(read_x_then_y);
  std::thread d(read_y_then_x);

  a.join();
  b.join();
  c.join();
  d.join();

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

  return 0;
}

上述代碼中的assert永遠不會觸發,由於while循環總能保證x或者y的值已經修改成true,若是線程c或d中有一個線程if條件不知足,那麼另外一個線程的if條件總能保障,因此最後z的值必定不爲0。請注意memory_order_seq_cst的語義須要在全部標記memory_order_seq_cst的操做上有單一的整體順序。併發

順序一致是最直觀的順序,可是也是最爲昂貴的內存順序,由於它要求全部線程之間的全局同步。在多處理器系統中,這可能須要處理器之間至關密集和耗時的通訊。app

鬆散順序

以鬆散順序執行的原子類型上的操做不參與synchronizes-with關係。單線程中的同一變量的操做仍然服從happens-before的關係,但相對於其餘線程的順序幾乎沒有任何要求。惟一的要求是,從同一線程對單個原子變量的訪問不能重排,一旦給定的線程已經看到了原子變量的特定值,該線程以後的讀取就不能獲取該變量更早的值。如下程序展示了這種鬆散性。函數

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


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

void write_x_then_y()
{
  x.store(true, std::memory_order_relaxed);
  y.store(true, std::memory_order_relaxed);
}

void read_x_then_y()
{
  while(!y.load(std::memory_order_relaxed)) ;
  if(x.load(std::memory_order_relaxed))
    ++ z;
}

int main()
{
  x = false;
  y = false;
  z = 0;
  std::thread a(write_x_then_y);
  std::thread b(read_x_then_y);

  a.join();
  b.join();
  assert(z.load() != 0);
}

這一次,assert可能會觸發。由於x和y是不一樣的變量,每一個操做所產生的值的可見性沒有順序的保證。性能

爲了理解鬆散順序是如何工做的,能夠想象每一個變量是一個小隔間裏使用記事本的人。在他的記事本上有一列值。你能夠打電話給他,要求他給你一個值,或者你能夠告訴他寫下了一個新值。若是你告訴他寫下新值,他就將其寫在列表的底部。若是你向他要一個值,他就爲你從列表之中讀取一個數字。第一次你和這我的交談,若是你向他要一個值,此時他可能從他的記事本上的列表裏任意選一個給你。若是你接着向他要另外一個值,他可能會再給你同一個值,或者從列表的下方給一個給你。他永遠不會給你一個在列表上更上面的值ui

獲取釋放順序

獲取釋放順序是鬆散順序的進步,操做仍然沒有總的順序,可是引入了一些同步。在這個順序模型下,原子載入是acquire操做memory_order_acquire,原子存儲是release操做memory_order_release,原子的讀,修改,寫操做是獲取,釋放或者二者兼有memory_order_acq_rel。不一樣的線程仍然能夠看到不一樣的順序,可是這些順序受到了限制。atom

#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)) ;
  if(y.load(std::memory_order_acquire))
  {
    ++ z;
  }
}

void read_y_then_x()
{
  while(!y.load(std::memory_order_seq_cst)) ;
  if(x.load(std::memory_order_seq_cst))
  {
    ++ z;
  }
}

int main()
{
  x = false;
  y = false;
  z = 0;
  std::thread a(write_x);
  std::thread b(write_y);
  std::thread c(read_x_then_y);
  std::thread d(read_y_then_x);

  a.join();
  b.join();
  c.join();
  d.join();

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

  return 0;
}

上述代碼中的斷言仍然可能觸發,由於對x的載入和對y的載入都讀取false也是有可能的。x與y由不一樣的線程寫入,因此每種狀況從釋放到獲取的順序對另外一個線程的操做是沒有影響的。線程

可是對於同一個線程來講,使用獲取-釋放操做能夠在鬆散操做之中施加順序。

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

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

void write_x_then_y()
{
  x.store(true, std::memory_order_relaxed);
  y.store(true, std::memory_order_release);
}

void read_y_then_x()
{
  while(!y.load(std::memory_order_acquire));
  if(x.load(std::memory_order_relaxed))
    ++ z;
}

int main()
{
  x = false;
  y = false;
  z = 0;
  std::thread a(write_x_then_y);
  std::thread b(read_y_then_x);
  a.join();
  b.join();

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

  return 0;
}

由於存儲使用memory_order_release而且載入使用memory_order_acquire,存儲與載入同步。對x的存儲發生在y的存儲以前,由於它們在同一個線程之中。由於對y的存儲與對y的載入同步,對x的載入必然讀到true,因此斷言並不會觸發。配合使用release和acquire能夠達到跨線程同步的功能,以下代碼所示:

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


std::atomic<int> data[5];
std::atomic<bool> sync1(false), sync2(false);

void thread_1()
{
  data[0].store(42, std::memory_order_relaxed);
  data[1].store(97, std::memory_order_relaxed);
  data[2].store(17, std::memory_order_relaxed);
  data[3].store(1, std::memory_order_relaxed);
  data[4].store(2, std::memory_order_relaxed);
  sync1.store(true, std::memory_order_release);
}

void thread_2()
{
  while(!sync1.load(std::memory_order_acquire)) ;
  sync2.store(true, std::memory_order_release);
}

void thread_3()
{
  while(!sync2.load(std::memory_order_acquire));
  assert(data[0].load(std::memory_order_relaxed) == 42);
  assert(data[1].load(std::memory_order_relaxed) == 97);
  assert(data[2].load(std::memory_order_relaxed) == 17);
  assert(data[3].load(std::memory_order_relaxed) == 1);
  assert(data[4].load(std::memory_order_relaxed) == 2);
}

int main()
{
  std::thread a(thread_1);
  std::thread b(thread_2);
  std::thread c(thread_3);

  a.join();
  b.join();
  c.join();

  return 0;
}

獲取釋放順序與MEMORY_ORDER_CONSUME的數據依賴

經過在載入上使用memory_order_consume以及在以前的存儲上使用memory_order_release,你能夠確保所指向的數據獲得正確的同步,而且無需再其餘非依賴的數據上強制任何同步需求。如下代碼展現了這種用途:

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

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->s = "hello world";
  a.store(99, std::memory_order_relaxed);
  // 由於這裏依賴了x,因此這一句代碼執行時保證了x已經初始化完畢,而且已經完成賦值。
  // 要點,有依賴關係的都已賦值完畢
  p.store(x, std::memory_order_release);
}

void use_x()
{
  X * x;
  while(!(x=p.load(std::memory_order_consume)))
    sleep(1);
  assert(x->i == 42);
  assert(x->s == "hello world");
  // 可能斷言出錯
  assert(a.load(std::memory_order_relaxed) == 99);
}

int main()
{
  std::thread t1(create_x);
  std::thread t2(use_x);
  t1.join();
  t2.join();
}

上述代碼中的前兩個斷言不會出錯,由於p的載入帶有對那些經過變量x的表達式的依賴。另外一方面,在a的值上的斷言或許會被觸發。此操做並不依賴從p載入的值,於是對讀到的值就沒有保證。

內存屏障

內存屏障分爲寫內存屏障和讀內存屏障。寫內存屏障std::atomic_thread_fence(std::memory_order_release)保證全部在屏障以前的寫入操做都會在屏障以後的寫入操做以前完成,而讀內存屏障std::atomic_thread_fence(std::memory_order_acquire)確保全部屏障以前的讀取操做都會在屏障以後的讀取操做前執行。內存屏障使得特定的操做沒法穿越。如下代碼演示了內存屏障的用法。

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

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

void write_x_then_y()
{
  x.store(true, std::memory_order_relaxed);
  std::atomic_thread_fence(std::memory_order_release);
  y.store(true, std::memory_order_release);
}

void read_y_then_x()
{
  while(!y.load(std::memory_order_acquire));
  std::atomic_thread_fence(std::memory_order_acquire);
  if(x.load(std::memory_order_relaxed))
    ++ z;
}

int main()
{
  x = false;
  y = false;
  z = 0;
  std::thread a(write_x_then_y);
  std::thread b(read_y_then_x);
  a.join();
  b.join();

  assert(z.load() != 0);
}

釋放屏障與獲取屏障同步,由於線程b中從y載入在線程a中存儲的值,這意味着線程a對x的存儲發生在線程b從x的load以前,因此讀取的值必定爲true,斷言永遠不會觸發。

參考: 《 C++併發編程實戰 》

相關文章
相關標籤/搜索