《C++ Concurrency in Action》读书笔记四 c++内存模型和原子类型

本章概要

* C++11的内存模型

* C++ STL提供的原子类型

* 原子类型的操作

* 如何使用这些操作了进行线程同步

1. 内存模型基础

内存模型分为两方面,一个是基础的结构方面,一个是并发方面

1)对象和内存地址

* 每个变量都是一个对象,包括对象的成员变量

* 每个对象都有至少一个内存地址

* 基本类型int, char等都有一个内存地址无论他们的尺寸,或者即便他们是数组

* 相邻的比特享用相同的内存地址

2)对象,内存地址和并发

多线程要修改相同的内存地址时候需要使用mutex防止资源竞争,或者使用原子操作

如果多个线程需要访问同一个内存地址,使用非原子操作,或者需要修改值,就会产生资源竞争。

3)修改顺序

c++程序中的每个对象都有一个定义好的修改顺序。虽然每次执行这个顺序可能改变,但在执行过程中所有线程都必须遵循该顺序。如果对象不是原子类型,你需要负责让所有访问该对象的线程都遵循特定的顺序。否则会产生资源竞争


2. c++中的原子操作和原子类型

原子操作就是不可再分割的操作

非原子操作就是对一个对象的操作分成多步完成,可以被分割。在多线程下会产生资源竞争。

在C++中你需要使用原子类型来完成原子操作

1)标准原子类型

定义在<atomic>中,所有对它们的操作都是原子操作。它们有is_lock_free()函数 判断对一个对象的操作是否是原子指令(返回true),或者是要依赖内部锁和库(返回false)

std::atomic_flag 唯一不支持is_lock_free()成员

剩余的原子类型都和std::atomic<>有关

标准原子类型和其对应的类型表

atomic_bool  std::atomic<bool>

atomic_char std::atomic<char>

...

标准原子类型是不可复制和赋值的。但他们支持隐式转换到对应的数据类型

支持以下成员

load(), store(), exchange(), compare_exchange_weak() compare_exchange_strong()

+=, -=, *=, !=  

++, --

fetch_add(), fetch_or()

std::atomic<> 的操作

存储操作, memory_order_relaxed,  memory_order_release, memory_order_seq_cst

加载操作, memory_order_relaxed,  memory_order_consume, memory_order_acquire 或者memory_order_seq_cst

读与修改写操作, memory_order_relaxed, memory_order_consume, memory_order_acquire, memory_order_release, memory_order_acq_rel, memory_order_seq_cst

对所有操作默认的顺序是memory_order_seq_cst


2) std::atomic_flag的操作

std::atomic_flag f = ATOMIC_FLAG_INIT;  //this init the flat to clear state.

支持3种操作,销毁(析构函数),清除clear(),设置他或访问之前的值test_and_set()

f.clear(std::memory_order_release);
bool x = f.test_and_set();
赋值和拷贝涉及两个对象的操作,分布进行因此不是原子操作。原子类型不支持赋值和拷贝
一个使用std::atomic_flag来充当互斥量的例子

class spinlock_mutex
{
    std::atomic_flag flag;
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);
    }
};

3)操作std::atomic<bool>

不支持拷贝构造和拷贝赋值, 但可以从一个非atomic<bool>对象构造或者赋值

std::atomic<bool>  b(true);

b = false;

该赋值操作符的返回类型不是原子类型的引用,返回的仅仅是一个值。

使用store() 来赋值

exchange()

load()读值

std::atomic<bool> b;
bool x = b.load(std::memory_order_acquire);
b.store(true);
x = b.exchange(false, std::memory_order_acq_rel);


根据现有的值来存储一个新值


这些操作叫compre/exchange 操作,是操作原子类型的奠基石。

compare_exchange_weak() compare_exchange_strong()

当前值与期望值相等时,修改当前值为设定值,返回true
当前值与期望值不等时,将期望值修改为当前值,返回false
这个函数可能在满足true的情况下仍然返回false,所以只能在循环里使用,否则可以使用它的strong版本

返回值是bool

compare_exchange_weak() store()操作可能不一定会成功即使原子类型的值和提供值相等。此时变量并未被修改compare_exchange_weak()返回false

大多数是由于目标机器缺少compare-and-exchange指令

如果处理器不能按照原子操作来处理该指令,而导致值被别的线程修改了,可能会返回一个虚假的失败


bool expected = false;
extern atomic<bool> b;
while(!b.compare_exchange_weak(expected, true) && !expected);


compare_exchange_strong() 只有当原子类型的值和目标值不一致时才返回false


compare/exchange操作可以带两个内存顺序参数。一个表示成功,一个表示失败

std::atomic<bool> b;

bool expected;

b.compare_exchange_weak(expected, true, memory_order_acq_rel, memory_order_acquire);

b.compare_exchange_weak(expected, true, memory_order_acq_rel);


4) std::atomic<T*>的操作

基本操作和std::atomic<bool>相同 不支持直接拷贝构造和赋值构造,但支持从指针构造。

is_lock_free(), load(), store(), exchange(), compare_exchange_weak(), compare_exchange_strong()等

还支持了指针的某些操作

fetch_add(), fetch_sub()

例如std::atomic<Foo*> x

x+=3; //指向第四个对象,返回第四个对象

x.fetch_add(3) //指向第四个对象,但返回第一个值的指针

class Foo{};

Foo some_array[5];
std::atomic<Foo*> p(some_array);
Foo* x = p.fetch_add(2);  //add 2 to p and return old value
assert(x == some_array);
assert(p.load() == &some_array[2]);
x = (p-=1);  //subtract 1 from p and return new value
assert(x == &some_array[1]);
assert(p.load() == &some_array[1]);
该函数也支持内存顺序

p.fetch_add(3, std::memory_order_release)


5) std::atomic<int>的操作

同上支持的一些操作load(), store(), exchange(), compare_exchange_weak(), compare_exchange_strong()等

还支持fetch_add(), fetch_sub(), fetch_and(), fetch_or(), fetch_xor() 还有一系列操作符

(+=, -=, &=, |=, ^=)

++, -- 

仅仅不支持,除号, 乘号,位移运算符等


6)std::atomic<> 类模板

支持用户定义类型

用户定义类型需要满足某些条件

一个默认的拷贝赋值操作符  (说明该类不能有虚函数或者虚基类)必须使用编译器默认生成的拷贝赋值操作符

可以使用memcpy()来进行赋值操作,就是不能有用户写的赋值语句。

最后该类型必须是(bitwise equality comparable 按位可比较) 支持memcmp() 为了支持compare/exchange工作

可以把符合要求的用户定义类型认为是一块内存区域

用户自定义类型std::atomic<T> 可以支持和bool相当的一些操作

7)atomic操作相关的函数

c++也支持一些对std::shared_ptr<>的原子操作 虽然std::shared_ptr<> 不是原子类型

std::shared_ptr<my_data> p;
void process_global_data()
{
    std::shared_ptr<my_data> local = std::atomic_load(&p);
    process_data(local);
}


void update_global_data()
{
    std::shared_ptr<my_data> local(new my_data);
    std::atomic_store(&p, local);
}

3)同步操作和强制顺序

#include <iostream>
#include <atomic>
#include <vector>
#include <mutex>
#include <thread>
#include <future>
#include <chrono>
#include <condition_variable>

std::vector<int> data;
std::atomic<bool> data_ready(false);

void reader_thread()
{
    while(!data_ready.load())
    {
        std::this_thread::sleep_for(std::chrono::milliseconds(1));
    }
    std::cout<<"The answer="<<data[0]<<std::endl;
}

void writer_thread()
{
    data.push_back(42);
    data_ready= true;
}

1)synchronize-with 关系

只适用在原子类型的操作中

2)happens-before 关系

一个操作A在顺序是优先于另外一个操作B,称为 A happens-before B

#include <iostream>

//using namespace std;

void foo(int a, int b)
{
    std::cout << a << "," << b << std::endl;
}

int get_num()
{
    static int i = 0;
    std::cout<<"call from get_num1"<<std::endl;
    return ++i;

}
int main()
{
    //cout << "Hello world!" << endl;
    foo(get_num(), get_num()); // calls to get_num are unorder
    return 0;
}
如果一个操作A在一个线程的顺序优先于另外一个线程的一个操作B ,A happens-before B

3)原子操作的内存序

memory_order_relaxed relaxed ordering

memory_order_consume acquire-release

memory_order_acquire acquire-release

memory_order_release same

memory_order_acq_rel same

memory_order_seq_cst  sequentially consistent


除非你特别指定,否则原子操作的内存顺序都是memory_order_seq_cst


例如在x86 x64的cpu上确定是否是原数操作使用acquire-release顺序 不需要额外的指令, 同时使用sequentially consist load操作不需要额外指令,但存储时需要额外的开销

不同情况选额不同的memory order能提高程序的执行效率


a . Sequentially consist ordering

简单来说就是,对所有以 memory_order_seq_cst 方式进行的内存操作,不管它们是不是分散在不同的 cpu 中同时进行,这些操作所产生的效果最终都要求有一个全局的顺序,而且这个顺序在各个相关的线程看起来是一致的。

始终顺序执行  意味着所有的操作都一定按照先后顺序执行。 这样多线程程序类似于单线程程序

sequentially consist ordering 的开支较大因为他要求同步所有的线程都按照顺序来执行。

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

//using namespace std;

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))
        ++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;
}
b. 非Sequentially consistent memory orderings

Threads don't have to agree on the order of events. 不同的线程不一定要遵循事件的顺序。

In the absence of the other ordering constraints, the only requirement is that all threads agree on the modification order of each individual variable.

线程遵循变量的修改顺序


c. Relaxed ordering

原子操作使用Relaxed ordering方式的话将不参与synchronize-with关系。在单线程中对同一变量的操作仍然遵循happens-before关系。

这表示一种最宽松的内存操作约定,该约定其实就是不进行约定,以这种方式修改内存时,不需要保证该修改会不会及时被其它线程看到,也不对乱序做任何要求,因此当对公共变量以 relaxed 方式进行读写时,编译器,cpu 等是被允许按照任意它们认为合适的方式来加以优化处理的。

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

//using namespace std;

std::atomic<int> x(0), y(0), z(0);
std::atomic<bool> go(false);

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 increament (std::atomic<int> * var_to_inc, read_values* values)
{
    while(!go)
        std::this_thread::yield();
    for(unsigned i = 0; i < loop_count ; ++i)
    {
        values[i].x = x.load(std::memory_order_relaxed);
        values[i].y = y.load(std::memory_order_relaxed);
        values[i].z = z.load(std::memory_order_relaxed);
        var_to_inc->store(i+1, std::memory_order_relaxed);
        std::this_thread::yield();
    }
}

void read_vals(read_values* values)
{
    while(!go)
        std::this_thread::yield();
    for(unsigned i = 0; i < loop_count ; ++i)
    {
        values[i].x = x.load(std::memory_order_relaxed);
        values[i].y = y.load(std::memory_order_relaxed);
        values[i].z = z.load(std::memory_order_relaxed);
        std::this_thread::yield();
    }
}

void print(read_values* v)
{
    for(unsigned i = 0; i<loop_count; ++i)
    {
        if(i)
            std::cout<<",";
        std::cout<<"("<<v[i].x<<","<<v[i].y<<","<<v[i].z<<")";
    }
    std::cout<<std::endl;
}


int main()
{
    std::thread t1(increament, &x, values1);
    std::thread t2(increament, &y, values2);
    std::thread t3(increament, &z, values3);
    std::thread t4(read_vals, values4);
    std::thread t5(read_vals, values5);

    go = true;

    t1.join();
    t2.join();
    t3.join();
    t4.join();
    t5.join();

    print(values1);
    print(values2);
    print(values3);
    print(values4);
    print(values5);
    return 0;
}

d. Acquire-release 顺序

原子load 是 Aqcuire顺序

原子Store是 release顺序

A release operation synchronizes-with an acquire operation that reads the value written.

但是仅仅限于同一thread。 不同thread之间还是relaxed关系

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

//using namespace std;

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_acquire));
    if(x.load(std::memory_order_acquire))
        ++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;
}

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

//using namespace std;

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


因为store x 和store y 在同一线程里,所以store x happens-before with store y

另外 store y 和 load y 是 synchronize-with 关系(因为是release-acquire的内存序)

而load y 和 load x 是 happens-before关系

所以store x 和 load x 也是happens-before关系 所以 最终load x的值一定是true


e. Transitive synchronization with acquire-release ordering

为了思考渡序,需要三个线程。

第一个线程修改一写共享数据做Store-release操作

第二个线程读取之前被做Store-release操作的共享数据,并使用load-acquire操作序然后再使用store-release操作另一个共享数据

第三个线程进行load-acquire操作读取第二个线程修改的共享数据


一个过渡序的例子

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

//using namespace std;

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

int num[5] = {42, 97, 17, -141, 2003};

void thread_1()
{
    for(int i = 0; i< 5; ++i)
        data[i].store(num[i], 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));
    for(int i = 0 ; i < 5 ; ++i)
        assert(data[i].load(std::memory_order_relaxed) == num[i]);
}

int main()
{
    std::thread t1(thread_1);
    std::thread t2(thread_2);
    std::thread t3(thread_3);

    t1.join();
    t2.join();
    t3.join();
    return 0;
}

虽然thread_2仅仅涉及了sync1 和sync2,单着足够满足thread_1和thread_2的同步关系。


这里也可以将sync1 和sync2 变成一个变量

"伪代码"

std::atomic<int> sync(0);

void thread_1()

{

....

sync.store(1, std::memory_order_release);

}


void thread_2()

{

    int expected = 1;

    while(!sync.compare_exchange_strong(expected, 2, std::memory_order_acquire))

        expected = 1;

}

void thread_3()

{

    while(sync.load(std::memory_order_acquire)<2)

....

}


对mutex的操作也是acquire-release  。lock 是 acquire 序

unlock 是release序

acquire-release序比sequentially consistent减少了许多同步上的开支


f. Acquire-relese序 和 Memory_order_consume序与数据的关系

memory_order_consume是基于数据的关系

有两种新的数据关系,dependency-ordered-before 和 carries-a-dependency-to

如果操作A的结果被用于操作B的操作数,那么称A carries-a-dependency-to B ,这种关系具有可传递性。即A carries-a-dependency-to B   ,  B carries-a-dependency-toC

那么 A carries-a-dependency-to C.

一个store操作A使用(release, acq_rel, relaxed或者seq_cst) dependency-ordered-before  load操作B(memory_order_consume)

如果A dependency-ordered-before B , 那么A也inter-thread happens-before B

#include <iostream>
#include <atomic>
#include <thread>
#include <chrono>
#include <assert.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";
    a.store(99, std::memory_order_relaxed);
    p.store(x, std::memory_order_release);
}

void use_x()
{
    X* x;
    while(!(x = p.load(std::memory_order_consume)))
        std::this_thread::sleep_for(std::chrono::microseconds(1));
    assert(x->i == 42);
    assert(x->s == "hello");
    assert(a.load(std::memory_order_relaxed) == 99);
}

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

    t1.join();
    t2.join();
    return 0;
}

有时候可以使用std::kill_dependency()来解除关系链

int global_data[] = {...}

std::atomic<int> index;

void f()

{

    int i = index.load(std::memory_order_consume);

    do_something_with(global_data[std::kill_dependency(i)]);

}


4) Release sequences 和 synchronizes-with


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

std::vector<int> queue_data;
std::atomic<int> count;

void populate_queue()
{
    unsigned const number_of_items = 20;
    queue_data.clear();
    for(unsigned i = 0; i < number_of_items; ++i)
    {
        queue_data.push_back(i);
    }
    count.store(number_of_items, std::memory_order_release);
}

void consume_queue_items()
{
    while(true)
    {
        int item_index;
        if((item_index = count.fetch_sub(1, std::memory_order_acquire))<=0)
        {
            wait_for_more_items();
            continue;
        }
        process(queue_data(item_index - 1));
    }
}

int main()
{
    std::thread a(populate_queue);
    std::thread b(consume_queue_items);
    std::thread c(consume_queue_items);

    a.join();
    b.join();
    c.join();
    return 0;
}

5) Fences

也称为“内存栅栏” 原子操作对数据的保护离不开fences,典型的原子操作用来隔离使用relaxed序。

release fence synchronizes-with acquire fence

#include <iostream>
#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);
    std::atomic_thread_fence(std::memory_order_release);
    y.store(true, std::memory_order_relaxed);
}

void read_y_then_x()
{
    while(!y.load(std::memory_order_relaxed));
    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);
    return 0;
}

6)使用atomic来顺序非atomic操作

结果和上面一样

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

bool x = false; // x is nonatomic type.
std::atomic<bool>  y;
std::atomic<int> z;

void write_x_then_y()
{
    x = true; // store x before the fence.
    std::atomic_thread_fence(std::memory_order_release);
    y.store(true, std::memory_order_relaxed); // store y after the fence.
}

void read_y_then_x()
{
    while(!y.load(std::memory_order_relaxed)); // wait until you see the write from #2
    std::atomic_thread_fence(std::memory_order_acquire);
    if(x)<span style="white-space:pre">	</span>// this will read the value written by #1
        ++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;
}


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值