C++ 内存模型

C++ std::atomic 原子类型

原子操作:一个不可分割的操作。
标准原子类型可以在头文件之中找到,在这种类型上的所有操作都是原子的。它们都有一个is_lock_free()的成员函数,让用户决定在给定类型上的操作是否用原子指令完成。唯一不提供is_lock_free()成员函数的类型是std::atomic_flag,在此类型上的操作要求是无锁的。可以利用std::atomic_flag实现一个简单的锁。

#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的操作上有单一的总体顺序。

顺序一致是最直观的顺序,但是也是最为昂贵的内存顺序,因为它要求所有线程之间的全局同步。在多处理器系统中,这可能需要处理器之间相当密集和耗时的通信。

松散顺序

以松散顺序执行的原子类型上的操作不参与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是不同的变量,每个操作所产生的值的可见性没有顺序的保证。

为了理解松散顺序是如何工作的,可以想象每个变量是一个小隔间里使用记事本的人。在他的记事本上有一列值。你可以打电话给他,要求他给你一个值,或者你可以告诉他写下了一个新值。如果你告诉他写下新值,他就将其写在列表的底部。如果你向他要一个值,他就为你从列表之中读取一个数字。第一次你和这个人交谈,如果你向他要一个值,此时他可能从他的记事本上的列表里任意选一个给你。如果你接着向他要另一个值,他可能会再给你同一个值,或者从列表的下方给一个给你。他永远不会给你一个在列表上更上面的值

获取释放顺序

获取释放顺序是松散顺序的进步,操作仍然没有总的顺序,但是引入了一些同步。在这个顺序模型下,原子载入是acquire操作memory_order_acquire,原子存储是release操作memory_order_release,原子的读,修改,写操作是获取,释放或者两者兼有memory_order_acq_rel。不同的线程仍然可以看到不同的顺序,但是这些顺序受到了限制。

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

posted @ 2017-05-29 00:56 ZHOU YANG 阅读(...) 评论(...) 编辑 收藏