《C++ concurrency in action》 读书笔记 -- Part 4 第五章 C++的多线程内存模型 (1)

《C++ concurreny in action》 第五章 C++的内存模型和原子操作

5.1 Memory model basics (内在模型基础)

Memory model 涉及两个方面:structural 和 concurrency

structural 是基础,主要是对象的布局

5.1.1 Objects and memory location

The C++ Standard defines an object as “a region of storage,”

clip_image001

注意四点:

  • 所有变量都有object,包括成员变量
  • 所有object都有自己的内存位置
  • 基础类型(int之类的)都有自己单独的内存区
  • bit field 共享同一个内存区

5.1.2 Objects, memory locations, and concurrency

产生race condition的条件就是多个线程访问同一个memory location,同时至少有一个在修改这个memory location的值。

必须要控制访问顺序来避免race condition。两种方法:1. 锁(mutex)2. 原子操作(atomic operation)

5.1.3 Modification orders

数据的修改顺序必须也有限制,否则会产生data race

5.2 Atomic operations and types in C++ (C++中的原子操作和类型)

An atomic operationis an indivisible operation.

原子操作就是不可分割的操作。要不就完成了, 要不就还没有做。不可能出现“只做了一半”的状态

在C++中,我们可以通过原子类型(atomic type)来进行原子操作。

5.2.1 Tthe standard atomic types

标准的原子类型都在头文件<atomic>中。这里头的类型的操作都是原子操作。

大多数都有 is_lock_free() 这个成员函数,如果返回 true ,则这个是“真正的原子操作(用的真正的原子操作指令)”,返回 false,则是使用锁来模拟。

只有std::atomic_flag不带有is_lock_free这个函数。因为这个类型必须是真正的原子操作。

其它的原子类型都是以std::atomic<>来实现的。

clip_image002

标准库中的原子类型都是不可拷贝和赋值的(not copyable or assignable)

原子类型的操作函数中都有一个memory-ordering的参数选项,可以精确控制 memory-ordering 语义。但这相关的主要在5.3节详述。

原子类型的操作分三类:

  • (存储操作)Store operations, 有这几个函数: memory_order_relaxed, memory_order_release, or memory_order_seq_cstordering
  • (Load操作?)Load operations,有这几个函数: memory_order_relaxed, memory_order_consume, memory_order_acquire,or memory_order_seq_cstordering
  • (修改操作)Read-modify-write operations, 有这几个函数:memory_order_relaxed, memory_

order_consume, memory_order_acquire, memory_order_release, memory_order_acq_rel,or memory_order_seq_cstordering

5.2.2 Operations on std::atomic_flag

std::atomic_flag 是标准库中最简单的原子类型,代表一个 bool 标志。这个类型的对象分两种状态:set 或者是 clear。这个类型的设计目的就是作为构建其它的原子类型,因此很少见它会被普通的程序使用。但因为很有代表性,所以本书从这个类型开始讲起。

std::atomic_flag的对象必须用 ATOMIC_FLAG_INIT进行初始化,初始化后对象会处于clear状态。(它是唯一一个对初始化有特殊要求的原子类型,但同时也是唯一一个会被保证是lock_free实现的原子类型)。静态类型的std::atomic_flag也会由编译器来保证初始化。

std::atomic_flag f=ATOMIC_FLAG_INIT;

一个被初始化后的std::atomic_flag对象可以做的三种操作有:

  • destroy(通过析构函数)
  • clear(通过 clear()函数)  store操作  参数可以指定memory-ordering tags,但是不能使用 memory_order_acquire or memory_order_acq_rel 这两种语义
  • set并读取状态(通过test_and_set()函数)read_modify_write操作,可以使用任何memory-ordering tags。
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);
    }
};

这个实现非常简陋,但已经可以足够用在std::lock_guard<>上,作为互斥锁来使用了。

std::atomic_flag的操作实在太有限,因此无法作为一个通用的bool标志来使用(因为没有一个单纯做值读取的操作)。如果需要通用的bool标志,那么应该使用 std::atomic<bool>。

5.2.3 Operations on std::atomic<bool>

std::atomic<boo>可以通过一个bool值来构建

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

std::atomic类型的赋值都是(非atomic类型的)返回值而不是引用。比如(std::atomic<int>的赋值操作返回的是int而不是int&),以避免在别的线程获取这个引用并通过非原子操作来修改它。

与std::atomic_flag不同,std::atomic<boo>通过以下几个方法来操作:

  • store()  => 赋值,可以指定memory_older
  • load() => 取得原子类型对象的值
  • exchange() => read_modify_write操作。

下面是代表示例:

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

std::atomic<boo>还有一些别的read_modify_write操作:

compare_exchange_weak( T& expected, T desired, ……)和compare_exchange_strong(T& expected, T desired, ……)

这两个函数我们现在只关注前两个参数(所以后面打了省略号,注意,这两个函数不是“可变参数函数哦~”),功能是这样:如果对象的值和expected一样,那么,就赋值成desired。而如果对象的值与expected不一样,则把expected的值赋值为现在对象的值。

(我:其实用std::atomic<bool>作为例子来讲这两个参数稍微有点点晦涩,用int的话好理解多了)

这两个函数的返回值都是bool类型,true代表store的操作进行了,false则没有进行。他们的区别在于:compare_exchange_weak可能对象值与expected一致,函数也可能会返回false,因为把desired赋值给对象会失败(特别是对于没有compare/exchange指令的CPU),失败的情况下不会更新std::atomic对象的值,compare_exchange_strong返回false则表示对象值与expected是不同的。

(这两个参数还可以指定memory_older,说实话,现在我基本上没看明白,还是看完5.3再回来消化吧。)

5.2.4 Operations on std::atomic<T*>: pointer arithmetic

指向一个T对象的指针的原子类型。基本上和std::atomic<bool>,有着上面介绍的所有操作。但多出了一些“指针运算操作”。fetch_add()和fetch_sub(),就“前进”和“后退”相应的距离。与有+=,-=和前、后缀的++和--。注意的是fetch_xx返回的是原来的值(而不是运算后的值)。

而+=,-=,++,--等的语义则与我们平常使用的指针是完全一致的。

class Foo{};
Foo some_array[5];
std::atomic<Foo*> p(some_array);
Foo* x=p.fetch_add(2); 
assert(x==some_array);
assert(p.load()==&some_array[2]);
x=(p-=1); 
assert(x==&some_array[1]);
assert(p.load()==&some_array[1]);

5.2.5 Operation on standard atomic integral types

其它的整形的原子类型的操作基本上就比较相同了,放在这一节进行一个概述:(load(),  store(),  exchange(),  compare_exchange_weak(), and compare_exchange_strong())之类上面介绍的操作当然都是有的。也有像:fetch_add(), fetch_sub(), fetch_and(), fetch_or(),
fetch_xor()这样的操作,分别代表了:(+=, -=, &=, |=, and ^=),还有前后缀的--,++。但没有乘,除和位运算。但由于原子类型一般主要用来计数,所以我们不会感觉到太多不便,实在需要的时候也可以使用compare_exchange_weak()加循环来得到。

5.2.6 The std::atomic<> primary class template

可以用atomic<>来做自定义的原子类型,但对于放入的模板参数有比较多的限制,我们可以这么认为:可以接受用浅拷贝和按位对比(bitwise compare)的类型才能作为atomic<T>中的T。(具体的说明请看原文)

5.2.7 Free functions for atomic operations

上面介绍的都是std::atomic的成员函数,其实它们都有相应对的free函数版本,支持情况如下表:

image

free函数设计得更为C一些,因此引用被换成了指针。

另外,C++标准库为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);
}

它们都是以std::shared_ptr<>*作为第一个参数的,主要有:load,store,exchange和compare/exchange。

 

 

posted on 2013-04-30 00:57  唐风思琪  阅读(3479)  评论(4编辑  收藏  举报

导航