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++并发编程实战 》

相关文章
相关标签/搜索