《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,”
注意四点:
- 所有变量都有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<>来实现的。
标准库中的原子类型都是不可拷贝和赋值的(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函数版本,支持情况如下表:
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。