c++ std::atomic类型以及其memory order介绍

C++ 11 atomic

简介

Atomic 类型是c++11里面引入的一种类型,它规定了当程序的多个线程同时访问一个变量的时候应该遵循的规则(通过memory order)。当访问某个atomic类型的对象的时候通过指定std::memory_order可能会建立线程间同步以及对非atomic变量的内存访问顺序。

std::atomic只可以用任何triviallyCopyable 的模板类型 T 实例化,在头文件里声明,其原型有以下4中形式:

template< class T >
struct atomic; (1)

template<>
struct atomic; (2)

template<>
struct atomic; (3)

template< class T >
struct atomic; (4)

TriviallyCopyable:

特别注意的是当使用第一种形式的时候模板类型T必须是triviallyCopyable,所谓triviallyCopyable必须满足以下6点要求:

(1) 每一个拷贝构造函数(copy constructor)是trivial

(2) 每一个移动构造函数(move constructor)是trivial

(3) 每个拷贝赋值表达式(copy assignment operator)是trivial

(4) 每一个移动复制表达式(move assignment operator)是rivial

(5) 至少有一个拷贝构造(copy constructor)、移动拷贝(move constructor)、拷贝赋值表达式(copy assignment)、移动赋值(move assignment)是non-deleted

(6) 析构函数是trivial

Trival 是甚麽意思:

Trival Copy Constructor,需要满足以下6个条件才算是Trivial Copy Constructor

(1) 拷贝构造函数不是程序员明确定义的(隐式的或是默认的)

(2) Class T 没有虚拟的成员函数(virtual member function)

(3) Class T没有虚拟基类

(4) Class T 的直接基类的拷贝构造函数是trivial

(5) Class T 的非静态类成员变量的拷贝构造函数是trivial

(6) Class T 没有非静态的volatile-qualified类型的成员变量

 

Example:

#include 
#include 
#include 

class B
{

public:

    virtual void fun() {}

};

 

class A
{

public:

    A() noexcept {}

    A(const A& other) : value_(other.value_) {}

    //virtual void fun() {}//compile error because of virtual member function

    int value_;

    B b;//compile error because of virtual member function of B

};

class C
{

public:
    virtual void func() {}
};

class D
{

public:

    virtual void fun() {}

};

class E : public D
{

};

 

int main()
{
    std::atomic a;
    std::atomic_bool boolValue;
    std::atomic c;

    A aa;

    aa.value_ = 3;

    a.store(aa);

   std::cout<<"whther class A is trivial type "<::value<::value<::value<::value<::value<


如上面的程序说明,当class A里面定义了virtual member function的时候,编译的时候会报错,另外可以用std::is_trivial来判断类型是否是trivial的,如果是trivial则其alue值为true否则为false。如上程序在gcc 4.9 c++11下面编译通过,输出为:

whether class A is trivial type 0

whether class B is trivial type 1

whether class C is trivial type 0

whether class D is trivial type 0

whether class E is trivial type 0

value_ is 3


这里我有个疑问,因为我的例子中class A并非Trivial但是还是可以实例化atomic结构?

memory_order

对于atomic对象操作有6memory ordering选项,memory_order_relaxed

memory_order_consumememory_order_acquirememory_order_releasememory_order_acq_relmemory_order_seq_cst默认情况下的为memory_order_seq_cst。尽管有6种选项,但是它们代表三种模型:sequentially-consistent ordering(memory_order_seq_cst)、acquire-release ordering(memory_order_consume,memory_order_acquire,memory_order_release, and memory_order_acq_rel)、relaxed ordering (memory_order_relaxed)。

另外需要注意的是对于不同的memory ordering运行 在不同的cpu架构的机器上运行的代价是不一样的,比如对于对同步指令的需求sequentially-consistent ordering模型大于acquire-release ordering或者relaxed orderingacquire-release ordering大于relaxed ordering;如果是运行在多处理器的操作系统上面,这些额外的同步指令开销可能会消耗重要的cpu时间,从而造成总体系统性能的下降。对于x86x86-64架构的处理器在使用acquire-release模型的时候不需要任何额外的指令开销,甚至是对于比较严格的sequentially consisten ordering也不需要特殊处理,并且花费的代价也很少。

sequentially consistent ordering

Sequentially consistent ordering atomic默认的操作选项,由于它暗含程序的行为对于atomic变量操作的一致性。如果atomic变量的所有操作顺序都确定了,那麼多线程程序行为就像在单线程内以特定的顺序执行这些操作即所有线程可以看到相同的操作执行顺序这也意味着操作不可以被reorder。通俗来说,如果1线程执行了AB两个操作,然后2线程执行B操作,那麼此时2线程必定知道A操作被执行过了,因为这个操作顺序(AB之前执行)是所有线程都能够看到的。比如下面的程序代码:

#include 

#include 
#include 

std::atomic x,y;
std::atomic z;

void write_x()
{

    x.store(true,std::memory_order_seq_cst);      #1

}

 

void write_y()
{

    y.store(true,std::memory_order_seq_cst);      #2

}

 

void read_x_then_y()
{

    while(!x.load(std::memory_order_seq_cst));

    if(y.load(std::memory_order_seq_cst))         #3

        ++z;

}

 

void read_y_then_x()
{

    while(!y.load(std::memory_order_seq_cst));

    if(x.load(std::memory_order_seq_cst))         #4

        ++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);                          #5

}


例子中的assert#5永远不可能发生,因为store to x和store to y肯定是首先发生(发生先后顺序未知),可以假设在read_x_then_y中 y.load为false,那麼可以确定x此时为true,也就是说sotre to x先于store y发生。那麼杂read_y_then_x中,x.load一定为true即++z肯定会被执行。这是因为由于操作一致性能被所有线程看见,那麼当y变为true(跳出循环)即sotre to y发生,由于store to x先于sotre to y所以此时d线程也看到此时x为true了。Sequentially consistent ordering是最直观的顺序,但也是最昂贵的操作。因为它需要对所有的线程进行同步,在一个多处理器的操作系统中,这些操作会消耗额外的时间用来处理器之间的通信。


relaxed ordering

relaxed ordering不会造成顺序同步,在单个线程类依然是遵循操作在前先执行的顺序,但是到其他线程里面则无法知道这种先后顺序即在1线程内A操作先于B操作,那麼在2线程里面执行B操作的时候它可能会认为A操作在此之前并没有被执行过。依然来看一个例子:

#include 

#include 
#include 

std::atomic x,y;
std::atomic 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;

    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);                        #5

}


在这个例子中,assert #5有可能发生,当在线程a里面两个操作都执行完之后,切换到线程b,当跳出循环判断x的时候,由于线程a内的两个赋值操作的顺序线程b并不知道,所以可能认为x仍然为false,从而最终被assert捕获。

 

为了更好的说明问题,下面用一个比较复杂的例子:

#include 
#include 
#include 

std::atomic x(0),y(0),z(0);                                   #1

std::atomic go(false);                                       #2

 

unsigned const loop_count=10;

 

struct read_values
{

    int x,y,z;

};

 

read_values values1[loop_count];

read_values values2[loop_count];

read_values values3[loop_count];

read_values values4[loop_count];

read_values values5[loop_count];

 

void increment(std::atomic* var_to_inc,read_values* values)
{

    while(!go)                                                           #3
        std::this_thread::yield();

    for(unsigned i=0;istore(i+1,std::memory_order_relaxed);             #4

        std::this_thread::yield();

    }

}

 

void read_vals(read_values* values)
{

    while(!go)                                                           #5
        std::this_thread::yield();

    for(unsigned i=0;i


三个共享的全局变量,5个线程。其中前三个线程读取全局变量值并且依次增加x,y,z的值。后面两个线程读取三个全局变量的值。程序的输出可能为:

(0,0,0),(1,0,0),(2,0,0),(3,0,0),(4,0,0),(5,7,0),(6,7,8),(7,9,8),(8,9,8),(9,9,10)

(0,0,0),(0,1,0),(0,2,0),(1,3,5),(8,4,5),(8,5,5),(8,6,6),(8,7,9),(10,8,9),(10,9,10)

(0,0,0),(0,0,1),(0,0,2),(0,0,3),(0,0,4),(0,0,5),(0,0,6),(0,0,7),(0,0,8),(0,0,9)

(1,3,0),(2,3,0),(2,4,1),(3,6,4),(3,9,5),(5,10,6),(5,10,8),(5,10,10),(9,10,10),(10,10,10)

(0,0,0),(0,0,0),(0,0,0),(6,3,7),(6,5,7),(7,7,7),(7,8,7),(8,8,7),(8,8,9),(8,8,9)


从输出上可以看到:

(1) 第一行的x,第二行的y,第三行的z都是依次递增上去的(因为对于单个赋值操作是在同一个线程中执行)。

(2) 元素x,y,z都是递增的(不可能出现减少的情况),虽然递增速度随机。

(3) 线程3中,x和y都没有更新,但实际上线程1,2都没有停止。

由于不同线程间没有固定的操作顺序,所以全局变量的读取具有随机性(取决与该线程内该变量甚麽时候更新),由于变量的值一直都是递增增加,所以更新的值也是递增的不可能出现在时刻i时x=5,到时刻y时x=3(y > x)的情况出现。有一个比较形象的比喻解释这个现象,可以打开本文的参考链接2中的Understanding Relaxed Ordering

从上面的例子中可以看出用memory_order_relaxed选项是很难处理预测的,它必须结合另外一些有更强的同步性选项才能使用, 一般不建议用这个选项除非你确定一定要用这个选项。

acquire-release

Acquire-release ordering,它和relaxed ordering一样同样没有全局性的操作顺序,但它引入了另外一些同步。在这种模式下atomic变量的load操作是acquire(memory_order_acquire)的,而变量的store是release(memory_order_release)的。C++规定release操作会同步到操作acquire操作。这就意味着不同线程仍然只能看到不同的操作顺序,但是这些顺序有更严格的限制了。下面仍然用例子进行说明:

#include 
#include 
#include 

 
std::atomic x,y;

std::atomic 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))              #1

        ++z;

}

 

void read_y_then_x()
{

    while(!y.load(std::memory_order_acquire));

    if(x.load(std::memory_order_acquire))              #2

        ++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);                               #3

}


这个例子中assert(#3)可能会发生,因为对x,y的写操作是在不同的线程中执行的所以x,y的同步操作是互不影响的


所以为了让assert不发生,可以将两个变量的写操作放到同一个线程中:

#include 
#include 
#include 


std::atomic x,y;

std::atomic z;

 

void write_x_then_y()
{

    x.store(true,std::memory_order_relaxed);             #1

    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);

    std::thread b(read_y_then_x);

    a.join();

    b.join();

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

}

 

#1 Spin, waiting for y to be set to true


此时即可保证y.load返回true后,x.load一定也是true

Transitive-Synchronization with acquire-release ordering

同步的可传递性,即不同线程间的同步具有可传递的特性。同样上例子:

std::atomic data[5];

std::atomic 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(-141,std::memory_order_relaxed);

    data[4].store(2003,std::memory_order_relaxed);

    sync1.store(true,std::memory_order_release);                #1

}

 

void thread_2()
{

    while(!sync1.load(std::memory_order_acquire));              #2

    sync2.store(true,std::memory_order_release);                #3

}

 

void thread_3()
{

    while(!sync2.load(std::memory_order_acquire));              #4

    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)==-141);

    assert(data[4].load(std::memory_order_relaxed)==2003);

}

 

#1 Set sync1

#2 Loop until sync1 is set

#3 Set sync2

#4 Loop until sync2 is set


如以上一段程序,第一个线程以relaxed orderin写数据到data全局变量中,然后以release方式将数据写如sync1变量中。第二个线程先等待sync1变量变为true,然后在以release方式写数据到sync2变量中。第三个线程则等待sync2被同步变为true,然后assert捕获。这个例子的输出assert将永远都不会发生,这是因为sync1.store发生在sync1.load之前,sync1.load发生在sync2.store之前,而sync2.store发生在sync2.load之前,所以在执行load data数组的数据的时候实际上就保证了,data.store发生在data.load之前了尽管data.store是以relaxed oredering的方式操作的。

 

 

 

 

                                                                                                  

 

 

 

 

参考资料:

http://en.cppreference.com/w/cpp/atomic/atomic

www.developerfusion.com/article/138018/memory-ordering-for-atomic-operations-in-c0x/

你可能感兴趣的:(Linux)