《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。