原子操作 是個不可分割的操作。 在系統(tǒng)的所有線程中,你是不可能觀察到原子操作完成了一半這種情況的; 它要么就是做了,要么就是沒做,只有這兩種可能。 如果從對象讀取值的加載操作是 原子 的,而且對這個對象的所有修改操作也是 原子 的, 那么加載操作得到的值要么是對象的初始值,要么是某次修改操作存入的值。
另一方面,非原子操作可能會被另一個線程觀察到只完成一半。 如果這個操作是一個存儲操作,那么其他線程看到的值,可能既不是存儲前的值,也不是存儲的值,而是別的什么值。 如果這個非原子操作是一個加載操作,它可能先取到對象的一部分,然后值被另一個線程修改,然后它再取到剩余的部分, 所以它取到的既不是第一個值,也不是第二個值,而是兩個值的某種組合。 正如第三章所講的,這一下成了一個容易出問題的競爭冒險, 但在這個層面上它可能就構成了 數(shù)據(jù)競爭 (見5.1節(jié)),就成了未定義行為。
在C++中,多數(shù)時候你需要一個原子類型來得到原子的操作,我們來看一下這些類型。
標準 原子類型 定義在頭文件<atomic>中。
這些類型上的所有操作都是原子的,在語言定義中只有這些類型的操作是原子的,不過你可以用互斥鎖來 模擬 原子操作。
實際上,標準原子類型自己的實現(xiàn)就可能是這樣模擬出來的:
它們(幾乎)都有一個is_lock_free()成員函數(shù),
這個函數(shù)讓用戶可以查詢某原子類型的操作是直接用的原子指令(x.is_lock_free()返回true),
還是編譯器和庫內(nèi)部用了一個鎖(x.is_lock_free()返回false)。
只用std::atomic_flag類型不提供is_lock_free()成員函數(shù)。這個類型是一個簡單的布爾標志,并且在這種類型上的操作都需要是無鎖的;當你有一個簡單無鎖的布爾標志時,你可以使用其實現(xiàn)一個簡單的鎖,并且實現(xiàn)其他基礎的原子類型。當你覺得“真的很簡單”時,就說明:在std::atomic_flag對象明確初始化后,做查詢和設置(使用test_and_set()成員函數(shù)),或清除(使用clear()成員函數(shù))都很容易。這就是:無賦值,無拷貝,沒有測試和清除,沒有其他任何操作。
剩下的原子類型都可以通過特化std::atomic<>類型模板而訪問到,并且擁有更多的功能,但可能不都是無鎖的(如之前解釋的那樣)。在最流行的平臺上,期望原子變量都是無鎖的內(nèi)置類型(例如std::atomic<int>和std::atomic<void*>),但這沒有必要。你在后面將會看到,每個特化接口所反映出的類型特點;位操作(如&=)就沒有為普通指針所定義,所以它也就不能為原子指針所定義。
除了直接使用std::atomic<>類型模板外,你可以使用在表5.1中所示的原子類型集。由于歷史原因,原子類型已經(jīng)添加入C++標準中,這些備選類型名可能參考相應的std::atomic<>特化類型,或是特化的基類。在同一程序中混合使用備選名與std::atomic<>特化類名,會使代碼的移植大打折扣。
表5.1 標準原子類型的備選名和與其相關的std::atomic<>特化類
| 原子類型 | 相關特化類 |
|---|---|
| atomic_bool | std::atomic<bool> |
| atomic_char | std::atomic<char> |
| atomic_schar | std::atomic<signed char> |
| atomic_uchar | std::atomic<unsigned char> |
| atomic_int | std::atomic<int> |
| atomic_uint | std::atomic<unsigned> |
| atomic_short | std::atomic<short> |
| atomic_ushort | std::atomic<unsigned short> |
| atomic_long | std::atomic<long> |
| atomic_ulong | std::atomic<unsigned long> |
| atomic_llong | std::atomic<long long> |
| atomic_ullong | std::atomic<unsigned long long> |
| atomic_char16_t | std::atomic<char16_t> |
| atomic_char32_t | std::atomic<char32_t> |
| atomic_wchar_t | std::atomic<wchar_t> |
C++標準庫不僅提供基本原子類型,還定義了與原子類型對應的非原子類型,就如同標準庫中的std::size_t。如表5.2所示這些類型:
表5.2 標準原子類型定義(typedefs)和對應的內(nèi)置類型定義(typedefs)
| 原子類型定義 | 標準庫中相關類型定義 |
|---|---|
| atomic_int_least8_t | int_least8_t |
| atomic_uint_least8_t | uint_least8_t |
| atomic_int_least16_t | int_least16_t |
| atomic_uint_least16_t | uint_least16_t |
| atomic_int_least32_t | int_least32_t |
| atomic_uint_least32_t | uint_least32_t |
| atomic_int_least64_t | int_least64_t |
| atomic_uint_least64_t | uint_least64_t |
| atomic_int_fast8_t | int_fast8_t |
| atomic_uint_fast8_t | uint_fast8_t |
| atomic_int_fast16_t | int_fast16_t |
| atomic_uint_fast16_t | uint_fast16_t |
| atomic_int_fast32_t | int_fast32_t |
| atomic_uint_fast32_t | uint_fast32_t |
| atomic_int_fast64_t | int_fast64_t |
| atomic_uint_fast64_t | uint_fast64_t |
| atomic_intptr_t | intptr_t |
| atomic_uintptr_t | uintptr_t |
| atomic_size_t | size_t |
| atomic_ptrdiff_t | ptrdiff_t |
| atomic_intmax_t | intmax_t |
| atomic_uintmax_t | uintmax_t |
好多種類型!不過,它們有一個相當簡單的模式;對于標準類型進行typedef T,相關的原子類型就在原來的類型名前加上atomic_的前綴:atomic_T。除了singed類型的縮寫是s,unsigned的縮寫是u,和long long的縮寫是llong之外,這種方式也同樣適用于內(nèi)置類型。對于std::atomic<T>模板,使用對應的T類型去特化模板的方式,要好于使用別名的方式。
通常,標準原子類型是不能拷貝和賦值,他們沒有拷貝構造函數(shù)和拷貝賦值操作。但是,因為可以隱式轉(zhuǎn)化成對應的內(nèi)置類型,所以這些類型依舊支持賦值,可以使用load()和store()成員函數(shù),exchange()、compare_exchange_weak()和compare_exchange_strong()。它們都支持復合賦值符:+=, -=, *=, |= 等等。并且使用整型和指針的特化類型還支持 ++ 和 --。當然,這些操作也有功能相同的成員函數(shù)所對應:fetch_add(), fetch_or() 等等。賦值操作和成員函數(shù)的返回值要么是被存儲的值(賦值操作),要么是操作前的值(命名函數(shù))。這就能避免賦值操作符返回引用。為了獲取存儲在引用的的值,代碼需要執(zhí)行單獨的讀操作,從而允許另一個線程在賦值和讀取進行的同時修改這個值,這也就為條件競爭打開了大門。
std::atomic<>類模板不僅僅一套特化的類型,其作為一個原發(fā)模板也可以使用用戶定義類型創(chuàng)建對應的原子變量。因為,它是一個通用類模板,操作被限制為load(),store()(賦值和轉(zhuǎn)換為用戶類型), exchange(), compare_exchange_weak()和compare_exchange_strong()。
每種函數(shù)類型的操作都有一個可選內(nèi)存排序參數(shù),這個參數(shù)可以用來指定所需存儲的順序。在5.3節(jié)中,會對存儲順序選項進行詳述。現(xiàn)在,只需要知道操作分為三類:
現(xiàn)在,讓我們來看一下每個標準原子類型進行的操作,就從std::atomic_flag開始吧。
std::atomic_flag是最簡單的標準原子類型,它表示了一個布爾標志。這個類型的對象可以在兩個狀態(tài)間切換:設置和清除。它就是那么的簡單,只作為一個構建塊存在。我從未期待這個類型被使用,除非在十分特別的情況下。正因如此,它將作為討論其他原子類型的起點,因為它會展示一些原子類型使用的通用策略。
std::atomic_flag類型的對象必須被ATOMIC_FLAG_INIT初始化。初始化標志位是“清除”狀態(tài)。這里沒得選擇;這個標志總是初始化為“清除”:
std::atomic_flag f = ATOMIC_FLAG_INIT;
這適用于任何對象的聲明,并且可在任意范圍內(nèi)。它是唯一需要以如此特殊的方式初始化的原子類型,但它也是唯一保證無鎖的類型。如果std::atomic_flag是靜態(tài)存儲的,那么就的保證其是靜態(tài)初始化的,也就意味著沒有初始化順序問題;在首次使用時,其都需要初始化。
當你的標志對象已初始化,那么你只能做三件事情:銷毀,清除或設置(查詢之前的值)。這些事情對應的函數(shù)分別是:clear()成員函數(shù),和test_and_set()成員函數(shù)。clear()和test_and_set()成員函數(shù)可以指定好內(nèi)存順序。clear()是一個存儲操作,所以不能有memory_order_acquire或memory_order_acq_rel語義,但是test_and_set()是一個“讀-改-寫”操作,所有可以應用于任何內(nèi)存順序標簽。每一個原子操作,默認的內(nèi)存順序都是memory_order_seq_cst。例如:
f.clear(std::memory_order_release); // 1
bool x=f.test_and_set(); // 2
這里,調(diào)用clear()①明確要求,使用釋放語義清除標志,當調(diào)用test_and_set()②使用默認內(nèi)存順序設置表示,并且檢索舊值。
你不能拷貝構造另一個std::atomic_flag對象;并且,你不能將一個對象賦予另一個std::atomic_flag對象。這并不是std::atomic_flag特有的,而是所有原子類型共有的。一個原子類型的所有操作都是原子的,因賦值和拷貝調(diào)用了兩個對象,這就就破壞了操作的原子性。在這樣的情況下,拷貝構造和拷貝賦值都會將第一個對象的值進行讀取,然后再寫入另外一個。對于兩個獨立的對象,這里就有兩個獨立的操作了,合并這兩個操作必定是不原子的。因此,操作就不被允許。
有限的特性集使得std::atomic_flag非常適合于作自旋互斥鎖。初始化標志是“清除”,并且互斥量處于解鎖狀態(tài)。為了鎖上互斥量,循環(huán)運行test_and_set()直到舊值為false,就意味著這個線程已經(jīng)被設置為true了。解鎖互斥量是一件很簡單的事情,將標志清除即可。實現(xiàn)如下面的程序清單所示:
清單5.1 使用std::atomic_flag實現(xiàn)自旋互斥鎖
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);
}
};
這樣的互斥量是最最基本的,但是它已經(jīng)足夠std::lock_guard<>使用了(詳見第3章)。其本質(zhì)就是在lock()中等待,所以這里幾乎不可能有競爭的存在,并且可以確?;コ?。當我們看到內(nèi)存順序語義時,你將會看到它們是如何對一個互斥鎖保證必要的強制順序的。這個例子將在5.3.6節(jié)中展示。
由于std::atomic_flag局限性太強,因為它沒有非修改查詢操作,它甚至不能像普通的布爾標志那樣使用。所以,你最好使用std::atomic<bool>,接下來讓我們看看應該如何使用它。
最基本的原子整型類型就是std::atomic<bool>。如你所料,它有著比std::atomic_flag更加齊全的布爾標志特性。雖然它依舊不能拷貝構造和拷貝賦值,但是你可以使用一個非原子的bool類型構造它,所以它可以被初始化為true或false,并且你也可以從一個非原子bool變量賦值給std::atomic<bool>的實例:
std::atomic<bool> b(true);
b=false;
另一件需要注意的事情時,非原子bool類型的賦值操作不同于通常的操作(轉(zhuǎn)換成對應類型的引用,再賦給對應的對象):它返回一個bool值來代替指定對象。這是在原子類型中,另一種常見的模式:賦值操作通過返回值(返回相關的非原子類型)完成,而非返回引用。如果一個原子變量的引用被返回了,任何依賴與這個賦值結果的代碼都需要顯式加載這個值,潛在的問題是,結果可能會被另外的線程所修改。通過使用返回非原子值進行賦值的方式,你可以避免這些多余的加載過程,并且得到的值就是實際存儲的值。
雖然有內(nèi)存順序語義指定,但是使用store()去寫入(true或false)還是好于std::atomic_flag中限制性很強的clear()。同樣的,test_and_set()函數(shù)也可以被更加通用的exchange()成員函數(shù)所替換,exchange()成員函數(shù)允許你使用你新選的值替換已存儲的值,并且自動的檢索原始值。std::atomic<bool>也支持對值的普通(不可修改)查找,其會將對象隱式的轉(zhuǎn)換為一個普通的bool值,或顯示的調(diào)用load()來完成。如你預期,store()是一個存儲操作,而load()是一個加載操作。exchange()是一個“讀-改-寫”操作:
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<bool>提供的exchange(),不僅僅是一個“讀-改-寫”的操作;它還介紹了一種新的存儲方式:當當前值與預期值一致時,存儲新值的操作。
存儲一個新值(或舊值)取決于當前值
這是一種新型操作,叫做“比較/交換”,它的形式表現(xiàn)為compare_exchange_weak()和compare_exchange_strong()成員函數(shù)?!氨容^/交換”操作是原子類型編程的基石;它比較原子變量的當前值和一個期望值,當兩值相等時,存儲提供值。當兩值不等,期望值就會被更新為原子變量中的值。“比較/交換”函數(shù)值是一個bool變量,當返回true時執(zhí)行存儲操作,當false則更新期望值。
對于compare_exchange_weak()函數(shù),當原始值與預期值一致時,存儲也可能會不成功;在這個例子中變量的值不會發(fā)生改變,并且compare_exchange_weak()的返回是false。這可能發(fā)生在缺少獨立“比較-交換”指令的機器上,當處理器不能保證這個操作能夠自動的完成——可能是因為線程的操作將指令隊列從中間關閉,并且另一個線程安排的指令將會被操作系統(tǒng)所替換(這里線程數(shù)多于處理器數(shù)量)。這被稱為“偽失敗”(spurious failure),因為造成這種情況的原因是時間,而不是變量值。
因為compare_exchange_weak()可以“偽失敗”,所以這里通常使用一個循環(huán):
bool expected=false;
extern atomic<bool> b; // 設置些什么
while(!b.compare_exchange_weak(expected,true) && !expected);
在這個例子中,循環(huán)中expected的值始終是false,表示compare_exchange_weak()會莫名的失敗。
另一方面,如果實際值與期望值不符,compare_exchange_strong()就能保證值返回false。這就能消除對循環(huán)的需要,就可以知道是否成功的改變了一個變量,或已讓另一個線程完成。
如果你想要改變變量值,且無論初始值是什么(可能是根據(jù)當前值更新了的值),更新后的期望值將會變更有用;經(jīng)歷每次循環(huán)的時候,期望值都會重新加載,所以當沒有其他線程同時修改期望時,循環(huán)中對compare_exchange_weak()或compare_exchange_strong()的調(diào)用都會在下一次(第二次)成功。如果值的計算很容易存儲,那么使用compare_exchange_weak()能更好的避免一個雙重循環(huán)的執(zhí)行,即使compare_exchange_weak()可能會“偽失敗”(因此compare_exchange_strong()包含一個循環(huán))。另一方面,如果值計算的存儲本身是耗時的,那么當期望值不變時,使用compare_exchange_strong()可以避免對值的重復計算。對于std::atomic<bool>這些都不重要——畢竟只可能有兩種值——但是對于其他的原子類型就有較大的影響了。
“比較/交換”函數(shù)很少對兩個擁有內(nèi)存順序的參數(shù)進行操作,這就就允許內(nèi)存順序語義在成功和失敗的例子中有所不同;其可能是對memory_order_acq_rel語義的一次成功調(diào)用,而對memory_order_relaxed語義的一次失敗的調(diào)動。一次失敗的“比較/交換”將不會進行存儲,所以“比較/交換”操作不能擁有memeory_order_release或memory_order_acq_rel語義。因此,這里不保證提供的這些值能作為失敗的順序。你也不能提供比成功順序更加嚴格的失敗內(nèi)存順序;當你需要memory_order_acquire或memory_order_seq_cst作為失敗語序,那必須要如同“指定它們是成功語序”那樣去做。
如果你沒有指定失敗的語序,那就假設和成功的順序是一樣的,除了release部分的順序:memory_order_release變成memory_order_relaxed,并且memoyr_order_acq_rel變成memory_order_acquire。如果你都不指定,他們默認順序?qū)閙emory_order_seq_cst,這個順序提供了對成功和失敗的全排序。下面對compare_exchange_weak()的兩次調(diào)用是等價的:
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);
我在5.3節(jié)中會詳解對于不同內(nèi)存順序選擇的結果。
std::atomic<bool>和std::atomic_flag的不同之處在于,std::atomic<bool>不是無鎖的;為了保證操作的原子性,其實現(xiàn)中需要一個內(nèi)置的互斥量。當處于特殊情況時,你可以使用is_lock_free()成員函數(shù),去檢查std::atomic<bool>上的操作是否無鎖。這是另一個,除了std::atomic_flag之外,所有原子類型都擁有的特征。
第二簡單的原子類型就是特化原子指針——std::atomic<T*>,接下來就看看它是如何工作的吧。
原子指針類型,可以使用內(nèi)置類型或自定義類型T,通過特化std::atomic<T*>進行定義,就如同使用bool類型定義std::atomic<bool>類型一樣。雖然接口幾乎一致,但是它的操作是對于相關的類型的指針,而非bool值本身。就像std::atomic<bool>,雖然它既不能拷貝構造,也不能拷貝賦值,但是他可以通過合適的類型指針進行構造和賦值。如同成員函數(shù)is_lock_free()一樣,std::atomic<T*>也有l(wèi)oad(), store(), exchange(), compare_exchange_weak()和compare_exchage_strong()成員函數(shù),與std::atomic<bool>的語義相同,獲取與返回的類型都是T*,而不是bool。
std::atomic<T*>為指針運算提供新的操作?;静僮饔衒etch_add()和fetch_sub()提供,它們在存儲地址上做原子加法和減法,為+=, -=, ++和--提供簡易的封裝。對于內(nèi)置類型的操作,如你所預期:如果x是std::atomic<Foo*>類型的數(shù)組的首地址,然后x+=3讓其偏移到第四個元素的地址,并且返回一個普通的Foo*類型值,這個指針值是指向數(shù)組中第四個元素。fetch_add()和fetch_sub()的返回值略有不同(所以x.ftech_add(3)讓x指向第四個元素,并且函數(shù)返回指向第一個元素的地址)。這種操作也被稱為“交換-相加”,并且這是一個原子的“讀-改-寫”操作,如同exchange()和compare_exchange_weak()/compare_exchange_strong()一樣。正像其他操作那樣,返回值是一個普通的T*值,而非是std::atomic<T*>對象的引用,所以調(diào)用代碼可以基于之前的值進行操作:
class Foo{};
Foo some_array[5];
std::atomic<Foo*> p(some_array);
Foo* x=p.fetch_add(2); // p加2,并返回原始值
assert(x==some_array);
assert(p.load()==&some_array[2]);
x=(p-=1); // p減1,并返回原始值
assert(x==&some_array[1]);
assert(p.load()==&some_array[1]);
函數(shù)也允許內(nèi)存順序語義作為給定函數(shù)的參數(shù):
p.fetch_add(3,std::memory_order_release);
因為fetch_add()和fetch_sub()都是“讀-改-寫”操作,它們可以擁有任意的內(nèi)存順序標簽,以及加入到一個釋放序列中。指定的語序不可能是操作符的形式,因為沒辦法提供必要的信息:這些形式都具有memory_order_seq_cst語義。
剩下的原子類型基本上都差不多:它們都是整型原子類型,并且都擁有同樣的接口(除了相關的內(nèi)置類型不一樣)。下面我們就看看這一類類型。
如同普通的操作集合一樣(load(), store(), exchange(), compare_exchange_weak(), 和compare_exchange_strong()),在std::atomic<int>和std::atomic<unsigned long long>也是有一套完整的操作可以供使用:fetch_add(), fetch_sub(), fetch_and(), fetch_or(), fetch_xor(),還有復合賦值方式((+=, -=, &=, |=和^=),以及++和--(++x, x++, --x和x--)。雖然對于普通的整型來說,這些復合賦值方式還不完全,但也十分接近完整了:只有除法、乘法和移位操作不在其中。因為,整型原子值通常用來作計數(shù)器,或者是掩碼,所以以上操作的缺失顯得不是那么重要;如果需要,額外的操作可以將compare_exchange_weak()放入循環(huán)中完成。
對于std::atomic<T*>類型緊密相關的兩個函數(shù)就是fetch_add()和fetch_sub();函數(shù)原子化操作,并且返回舊值,而符合賦值運算會返回新值。前綴加減和后綴加減與普通用法一樣:++x對變量進行自加,并且返回新值;而x++對變量自加,返回舊值。正如你預期的那樣,在這兩個例子中,結果都是相關整型的一個值。
我們已經(jīng)看過所有基本原子類型;剩下的就是std::atomic<>類型模板,而非其特化類型。那么接下來讓我們來了解一下std::atomic<>類型模板。
主模板的存在,在除了標準原子類型之外,允許用戶使用自定義類型創(chuàng)建一個原子變量。不是任何自定義類型都可以使用std::atomic<>的:需要滿足一定的標準才行。為了使用std::atomic<UDT>(UDT是用戶定義類型),這個類型必須有拷貝賦值運算符。這就意味著這個類型不能有任何虛函數(shù)或虛基類,以及必須使用編譯器創(chuàng)建的拷貝賦值操作。不僅僅是這些,自定義類型中所有的基類和非靜態(tài)數(shù)據(jù)成員也都需要支持拷貝賦值操作。這(基本上)就允許編譯器使用memcpy(),或賦值操作的等價操作,因為它們的實現(xiàn)中沒有用戶代碼。
最后,這個類型必須是“位可比的”(bitwise equality comparable)。這與對賦值的要求差不多;你不僅需要確定,一個UDT類型對象可以使用memcpy()進行拷貝,還要確定其對象可以使用memcmp()對位進行比較。之所以要求這么多,是為了保證“比較/交換”操作能正常的工作。
以上嚴格的限制都是依據(jù)第3章中的一個建議:不要將鎖定區(qū)域內(nèi)的數(shù)據(jù),以引用或指針的形式,作為參數(shù)傳遞給用戶提供的函數(shù)。通常情況下,編譯器不會為std::atomic<UDT>類型生成無鎖代碼,所以它將對所有操作使用一個內(nèi)部鎖。如果用戶提供的拷貝賦值或比較操作被允許,那么這就需要傳遞保護數(shù)據(jù)的引用作為一個參數(shù),這就有悖于指導意見了。當原子操作需要時,運行庫也可自由的使用單鎖,并且運行庫允許用戶提供函數(shù)持有鎖,這樣就有可能產(chǎn)生死鎖(或因為做一個比較操作,而阻塞了其他的線程)。最終,因為這些限制可以讓編譯器將用戶定義的類型看作為一組原始字節(jié),所以編譯器可以對std::atomic<UDT>直接使用原子指令(因此實例化一個特殊無鎖結構)。
注意,雖然使用std::atomic<float>或std::atomic<double>(內(nèi)置浮點類型滿足使用memcpy和memcmp的標準),但是它們在compare_exchange_strong函數(shù)中的表現(xiàn)可能會令人驚訝。當存儲的值與當前值相等時,這個操作也可能失敗,可能因為舊值是一個不同方式的表達。這就不是對浮點數(shù)的原子計算操作了。在使用compare_exchange_strong函數(shù)的過程中,你可能會遇到相同的結果,如果你使用std::atomic<>特化一個用戶自定義類型,且這個類型定義了比較操作,而這個比較操作與memcmp又有不同——操作可能會失敗,因為兩個相等的值擁有不同的表達方式。
如果你的UDT類型的大小如同(或小于)一個int或void*類型時,大多數(shù)平臺將會對std::atomic<UDT>使用原子指令。有些平臺可能會對用戶自定義類型(兩倍于int或void*的大小)特化的std::atmic<>使用原子指令。這些平臺通常支持所謂的“雙字節(jié)比較和交換”(double-word-compare-and-swap,DWCAS)指令,這個指令與compare_exchange_xxx相關聯(lián)著。這種指令的支持,對于寫無鎖代碼是有很大的幫助,具體的內(nèi)容會在第7章討論。
以上的限制也意味著有些事情你不能做,比如,創(chuàng)建一個std::atomic<std::vector<int>>類型。這里不能使用包含有計數(shù)器,標志指針和簡單數(shù)組的類型,作為特化類型。雖然這不會導致任何問題,但是,越是復雜的數(shù)據(jù)結構,就有越多的操作要去做,而非只有賦值和比較。如果這種情況發(fā)生了,你最好使用std::mutex保證數(shù)據(jù)能被必要的操作所保護,就像第3章描述的。
當使用用戶定義類型T進行實例化時,std::atomic<T>的可用接口就只有: load(), store(), exchange(), compare_exchange_weak(), compare_exchange_strong()和賦值操作,以及向類型T轉(zhuǎn)換的操作。表5.3列舉了每一個原子類型所能使用的操作。
http://wiki.jikexueyuan.com/project/cplusplus-concurrency-action/images/chapter5/5-3-table.png" alt="" />
表5.3 每一個原子類型所能使用的操作
直到現(xiàn)在,我都還沒有去描述成員函數(shù)對原子類型操作的形式。但是,在不同的原子類型中也有等價的非成員函數(shù)存在。大多數(shù)非成員函數(shù)的命名與對應成員函數(shù)有關,但是需要“atomic_”作為前綴(比如,std::atomic_load())。這些函數(shù)都會被不同的原子類型所重載。在指定一個內(nèi)存序列標簽時,他們會分成兩種:一種沒有標簽,另一種將“_explicit”作為后綴,并且需要一個額外的參數(shù),或?qū)?nèi)存順序作為標簽,亦或只有標簽(例如,std::atomic_store(&atomic_var,new_value)與std::atomic_store_explicit(&atomic_var,new_value,std::memory_order_release)。不過,原子對象被成員函數(shù)隱式引用,所有釋放函數(shù)都持有一個指向原子對象的指針(作為第一個參數(shù))。
例如,std::atomic_is_lock_free()只有一種類型(雖然會被其他類型所重載),并且對于同一個對象a,std::atomic_is_lock_free(&a)返回值與a.is_lock_free()相同。同樣的,std::atomic_load(&a)和a.load()的作用一樣,但需要注意的是,與a.load(std::memory_order_acquire)等價的操作是std::atomic_load_explicit(&a, std::memory_order_acquire)。
釋放函數(shù)的設計是為了要與C語言兼容,在C中只能使用指針,而不能使用引用。例如,compare_exchange_weak()和compare_exchange_strong()成員函數(shù)的第一個參數(shù)(期望值)是一個引用,而std::atomic_compare_exchange_weak()(第一個參數(shù)是指向?qū)ο蟮闹羔?的第二個參數(shù)是一個指針。std::atomic_compare_exchange_weak_explicit()也需要指定成功和失敗的內(nèi)存序列,而“比較/交換”成員函數(shù)都有一個單內(nèi)存序列形式(默認是std::memory_order_seq_cst),重載函數(shù)可以分別獲取成功和失敗內(nèi)存序列。
對std::atomic_flag的操作是“反潮流”的,在那些操作中它們“標志”的名稱為:std::atomic_flag_test_and_set()和std::atomic_flag_clear(),但是以“_explicit”為后綴的額外操作也能夠指定內(nèi)存順序:std::atomic_flag_test_and_set_explicit()和std::atomic_flag_clear_explicit()。
C++標準庫也對在一個原子類型中的std::shared_ptr<>智能指針類型提供釋放函數(shù)。這打破了“只有原子類型,才能提供原子操作”的原則,這里std::shared_ptr<>肯定不是原子類型。但是,C++標準委員會感覺對此提供額外的函數(shù)是很重要的??墒褂玫脑硬僮饔校簂oad, store, exchange和compare/exchange,這些操作重載了標準原子類型的操作,并且獲取一個std::shared_ptr<>*作為第一個參數(shù):
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);
}
作為和原子操作一同使用的其他類型,也提供“_explicit”變量,允許你指定所需的內(nèi)存順序,并且std::atomic_is_lock_free()函數(shù)可以用來確定實現(xiàn)是否使用鎖,來保證原子性。
如之前的描述,標準原子類型不僅僅是為了避免數(shù)據(jù)競爭所造成的未定義操作,它們還允許用戶對不同線程上的操作進行強制排序。這種強制排序是數(shù)據(jù)保護和同步操作的基礎,例如,std::mutex和std::future<>。所以,讓我繼續(xù)了解本章的真實意義:內(nèi)存模型在并發(fā)方面的細節(jié),如何使用原子操作同步數(shù)據(jù)和強制排序。