鍍金池/ 教程/ C/ 5.2 <code>C++</code>中的原子操作和原子類型
3.4 本章總結(jié)
6.3 基于鎖設(shè)計(jì)更加復(fù)雜的數(shù)據(jù)結(jié)構(gòu)
6.1 為并發(fā)設(shè)計(jì)的意義何在?
5.2 <code>C++</code>中的原子操作和原子類型
A.7 自動(dòng)推導(dǎo)變量類型
2.1 線程管理的基礎(chǔ)
8.5 在實(shí)踐中設(shè)計(jì)并發(fā)代碼
2.4 運(yùn)行時(shí)決定線程數(shù)量
2.2 向線程函數(shù)傳遞參數(shù)
第4章 同步并發(fā)操作
2.3 轉(zhuǎn)移線程所有權(quán)
8.3 為多線程性能設(shè)計(jì)數(shù)據(jù)結(jié)構(gòu)
6.4 本章總結(jié)
7.3 對(duì)于設(shè)計(jì)無鎖數(shù)據(jù)結(jié)構(gòu)的指導(dǎo)建議
關(guān)于這本書
A.1 右值引用
2.6 本章總結(jié)
D.2 &lt;condition_variable&gt;頭文件
A.6 變參模板
6.2 基于鎖的并發(fā)數(shù)據(jù)結(jié)構(gòu)
4.5 本章總結(jié)
A.9 本章總結(jié)
前言
第10章 多線程程序的測(cè)試和調(diào)試
5.4 本章總結(jié)
第9章 高級(jí)線程管理
5.1 內(nèi)存模型基礎(chǔ)
2.5 識(shí)別線程
第1章 你好,C++的并發(fā)世界!
1.2 為什么使用并發(fā)?
A.5 Lambda函數(shù)
第2章 線程管理
4.3 限定等待時(shí)間
D.3 &lt;atomic&gt;頭文件
10.2 定位并發(fā)錯(cuò)誤的技術(shù)
附錄B 并發(fā)庫的簡(jiǎn)單比較
5.3 同步操作和強(qiáng)制排序
A.8 線程本地變量
第8章 并發(fā)代碼設(shè)計(jì)
3.3 保護(hù)共享數(shù)據(jù)的替代設(shè)施
附錄D C++線程庫參考
第7章 無鎖并發(fā)數(shù)據(jù)結(jié)構(gòu)設(shè)計(jì)
D.7 &lt;thread&gt;頭文件
D.1 &lt;chrono&gt;頭文件
4.1 等待一個(gè)事件或其他條件
A.3 默認(rèn)函數(shù)
附錄A 對(duì)<code>C++</code>11語言特性的簡(jiǎn)要介紹
第6章 基于鎖的并發(fā)數(shù)據(jù)結(jié)構(gòu)設(shè)計(jì)
封面圖片介紹
7.2 無鎖數(shù)據(jù)結(jié)構(gòu)的例子
8.6 本章總結(jié)
8.1 線程間劃分工作的技術(shù)
4.2 使用期望等待一次性事件
8.4 設(shè)計(jì)并發(fā)代碼的注意事項(xiàng)
D.5 &lt;mutex&gt;頭文件
3.1 共享數(shù)據(jù)帶來的問題
資源
9.3 本章總結(jié)
10.3 本章總結(jié)
10.1 與并發(fā)相關(guān)的錯(cuò)誤類型
D.4 &lt;future&gt;頭文件
3.2 使用互斥量保護(hù)共享數(shù)據(jù)
9.1 線程池
1.1 何謂并發(fā)
9.2 中斷線程
4.4 使用同步操作簡(jiǎn)化代碼
A.2 刪除函數(shù)
1.3 C++中的并發(fā)和多線程
1.4 開始入門
第5章 C++內(nèi)存模型和原子類型操作
消息傳遞框架與完整的ATM示例
8.2 影響并發(fā)代碼性能的因素
7.1 定義和意義
D.6 &lt;ratio&gt;頭文件
A.4 常量表達(dá)式函數(shù)
7.4 本章總結(jié)
1.5 本章總結(jié)
第3章 線程間共享數(shù)據(jù)

5.2 <code>C++</code>中的原子操作和原子類型

原子操作 是個(gè)不可分割的操作。 在系統(tǒng)的所有線程中,你是不可能觀察到原子操作完成了一半這種情況的; 它要么就是做了,要么就是沒做,只有這兩種可能。 如果從對(duì)象讀取值的加載操作是 原子 的,而且對(duì)這個(gè)對(duì)象的所有修改操作也是 原子 的, 那么加載操作得到的值要么是對(duì)象的初始值,要么是某次修改操作存入的值。

另一方面,非原子操作可能會(huì)被另一個(gè)線程觀察到只完成一半。 如果這個(gè)操作是一個(gè)存儲(chǔ)操作,那么其他線程看到的值,可能既不是存儲(chǔ)前的值,也不是存儲(chǔ)的值,而是別的什么值。 如果這個(gè)非原子操作是一個(gè)加載操作,它可能先取到對(duì)象的一部分,然后值被另一個(gè)線程修改,然后它再取到剩余的部分, 所以它取到的既不是第一個(gè)值,也不是第二個(gè)值,而是兩個(gè)值的某種組合。 正如第三章所講的,這一下成了一個(gè)容易出問題的競(jìng)爭(zhēng)冒險(xiǎn), 但在這個(gè)層面上它可能就構(gòu)成了 數(shù)據(jù)競(jìng)爭(zhēng) (見5.1節(jié)),就成了未定義行為。

C++中,多數(shù)時(shí)候你需要一個(gè)原子類型來得到原子的操作,我們來看一下這些類型。

5.2.1 標(biāo)準(zhǔn)原子類型

標(biāo)準(zhǔn) 原子類型 定義在頭文件<atomic>中。 這些類型上的所有操作都是原子的,在語言定義中只有這些類型的操作是原子的,不過你可以用互斥鎖來 模擬 原子操作。 實(shí)際上,標(biāo)準(zhǔn)原子類型自己的實(shí)現(xiàn)就可能是這樣模擬出來的: 它們(幾乎)都有一個(gè)is_lock_free()成員函數(shù), 這個(gè)函數(shù)讓用戶可以查詢某原子類型的操作是直接用的原子指令(x.is_lock_free()返回true), 還是編譯器和庫內(nèi)部用了一個(gè)鎖(x.is_lock_free()返回false)。

只用std::atomic_flag類型不提供is_lock_free()成員函數(shù)。這個(gè)類型是一個(gè)簡(jiǎn)單的布爾標(biāo)志,并且在這種類型上的操作都需要是無鎖的;當(dāng)你有一個(gè)簡(jiǎn)單無鎖的布爾標(biāo)志時(shí),你可以使用其實(shí)現(xiàn)一個(gè)簡(jiǎn)單的鎖,并且實(shí)現(xiàn)其他基礎(chǔ)的原子類型。當(dāng)你覺得“真的很簡(jiǎn)單”時(shí),就說明:在std::atomic_flag對(duì)象明確初始化后,做查詢和設(shè)置(使用test_and_set()成員函數(shù)),或清除(使用clear()成員函數(shù))都很容易。這就是:無賦值,無拷貝,沒有測(cè)試和清除,沒有其他任何操作。

剩下的原子類型都可以通過特化std::atomic<>類型模板而訪問到,并且擁有更多的功能,但可能不都是無鎖的(如之前解釋的那樣)。在最流行的平臺(tái)上,期望原子變量都是無鎖的內(nèi)置類型(例如std::atomic<int>std::atomic<void*>),但這沒有必要。你在后面將會(huì)看到,每個(gè)特化接口所反映出的類型特點(diǎn);位操作(如&=)就沒有為普通指針?biāo)x,所以它也就不能為原子指針?biāo)x。

除了直接使用std::atomic<>類型模板外,你可以使用在表5.1中所示的原子類型集。由于歷史原因,原子類型已經(jīng)添加入C++標(biāo)準(zhǔn)中,這些備選類型名可能參考相應(yīng)的std::atomic<>特化類型,或是特化的基類。在同一程序中混合使用備選名與std::atomic<>特化類名,會(huì)使代碼的移植大打折扣。

表5.1 標(biāo)準(zhǔn)原子類型的備選名和與其相關(guān)的std::atomic<>特化類

原子類型 相關(guān)特化類
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++標(biāo)準(zhǔn)庫不僅提供基本原子類型,還定義了與原子類型對(duì)應(yīng)的非原子類型,就如同標(biāo)準(zhǔn)庫中的std::size_t。如表5.2所示這些類型:

表5.2 標(biāo)準(zhǔn)原子類型定義(typedefs)和對(duì)應(yīng)的內(nèi)置類型定義(typedefs)

原子類型定義 標(biāo)準(zhǔn)庫中相關(guān)類型定義
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

好多種類型!不過,它們有一個(gè)相當(dāng)簡(jiǎn)單的模式;對(duì)于標(biāo)準(zhǔn)類型進(jìn)行typedef T,相關(guān)的原子類型就在原來的類型名前加上atomic_的前綴:atomic_T。除了singed類型的縮寫是s,unsigned的縮寫是u,和long long的縮寫是llong之外,這種方式也同樣適用于內(nèi)置類型。對(duì)于std::atomic<T>模板,使用對(duì)應(yīng)的T類型去特化模板的方式,要好于使用別名的方式。

通常,標(biāo)準(zhǔn)原子類型是不能拷貝和賦值,他們沒有拷貝構(gòu)造函數(shù)和拷貝賦值操作。但是,因?yàn)榭梢噪[式轉(zhuǎn)化成對(duì)應(yīng)的內(nèi)置類型,所以這些類型依舊支持賦值,可以使用load()和store()成員函數(shù),exchange()、compare_exchange_weak()和compare_exchange_strong()。它們都支持復(fù)合賦值符:+=, -=, *=, |= 等等。并且使用整型和指針的特化類型還支持 ++ 和 --。當(dāng)然,這些操作也有功能相同的成員函數(shù)所對(duì)應(yīng):fetch_add(), fetch_or() 等等。賦值操作和成員函數(shù)的返回值要么是被存儲(chǔ)的值(賦值操作),要么是操作前的值(命名函數(shù))。這就能避免賦值操作符返回引用。為了獲取存儲(chǔ)在引用的的值,代碼需要執(zhí)行單獨(dú)的讀操作,從而允許另一個(gè)線程在賦值和讀取進(jìn)行的同時(shí)修改這個(gè)值,這也就為條件競(jìng)爭(zhēng)打開了大門。

std::atomic<>類模板不僅僅一套特化的類型,其作為一個(gè)原發(fā)模板也可以使用用戶定義類型創(chuàng)建對(duì)應(yīng)的原子變量。因?yàn)?,它是一個(gè)通用類模板,操作被限制為load(),store()(賦值和轉(zhuǎn)換為用戶類型), exchange(), compare_exchange_weak()和compare_exchange_strong()。

每種函數(shù)類型的操作都有一個(gè)可選內(nèi)存排序參數(shù),這個(gè)參數(shù)可以用來指定所需存儲(chǔ)的順序。在5.3節(jié)中,會(huì)對(duì)存儲(chǔ)順序選項(xiàng)進(jìn)行詳述?,F(xiàn)在,只需要知道操作分為三類:

  1. Store操作,可選如下順序:memory_order_relaxed, memory_order_release, memory_order_seq_cst。
  2. Load操作,可選如下順序:memory_order_relaxed, memory_order_consume, memory_order_acquire, memory_order_seq_cst。
  3. Read-modify-write(讀-改-寫)操作,可選如下順序:memory_order_relaxed, memory_order_consume, memory_order_acquire, memory_order_release, memory_order_acq_rel, memory_order_seq_cst。
    所有操作的默認(rèn)順序都是memory_order_seq_cst。

現(xiàn)在,讓我們來看一下每個(gè)標(biāo)準(zhǔn)原子類型進(jìn)行的操作,就從std::atomic_flag開始吧。

5.2.2 std::atomic_flag的相關(guān)操作

std::atomic_flag是最簡(jiǎn)單的標(biāo)準(zhǔn)原子類型,它表示了一個(gè)布爾標(biāo)志。這個(gè)類型的對(duì)象可以在兩個(gè)狀態(tài)間切換:設(shè)置和清除。它就是那么的簡(jiǎn)單,只作為一個(gè)構(gòu)建塊存在。我從未期待這個(gè)類型被使用,除非在十分特別的情況下。正因如此,它將作為討論其他原子類型的起點(diǎn),因?yàn)樗鼤?huì)展示一些原子類型使用的通用策略。

std::atomic_flag類型的對(duì)象必須被ATOMIC_FLAG_INIT初始化。初始化標(biāo)志位是“清除”狀態(tài)。這里沒得選擇;這個(gè)標(biāo)志總是初始化為“清除”:

std::atomic_flag f = ATOMIC_FLAG_INIT;

這適用于任何對(duì)象的聲明,并且可在任意范圍內(nèi)。它是唯一需要以如此特殊的方式初始化的原子類型,但它也是唯一保證無鎖的類型。如果std::atomic_flag是靜態(tài)存儲(chǔ)的,那么就的保證其是靜態(tài)初始化的,也就意味著沒有初始化順序問題;在首次使用時(shí),其都需要初始化。

當(dāng)你的標(biāo)志對(duì)象已初始化,那么你只能做三件事情:銷毀,清除或設(shè)置(查詢之前的值)。這些事情對(duì)應(yīng)的函數(shù)分別是:clear()成員函數(shù),和test_and_set()成員函數(shù)。clear()和test_and_set()成員函數(shù)可以指定好內(nèi)存順序。clear()是一個(gè)存儲(chǔ)操作,所以不能有memory_order_acquire或memory_order_acq_rel語義,但是test_and_set()是一個(gè)“讀-改-寫”操作,所有可以應(yīng)用于任何內(nèi)存順序標(biāo)簽。每一個(gè)原子操作,默認(rèn)的內(nèi)存順序都是memory_order_seq_cst。例如:

f.clear(std::memory_order_release);  // 1
bool x=f.test_and_set();  // 2

這里,調(diào)用clear()①明確要求,使用釋放語義清除標(biāo)志,當(dāng)調(diào)用test_and_set()②使用默認(rèn)內(nèi)存順序設(shè)置表示,并且檢索舊值。

你不能拷貝構(gòu)造另一個(gè)std::atomic_flag對(duì)象;并且,你不能將一個(gè)對(duì)象賦予另一個(gè)std::atomic_flag對(duì)象。這并不是std::atomic_flag特有的,而是所有原子類型共有的。一個(gè)原子類型的所有操作都是原子的,因賦值和拷貝調(diào)用了兩個(gè)對(duì)象,這就就破壞了操作的原子性。在這樣的情況下,拷貝構(gòu)造和拷貝賦值都會(huì)將第一個(gè)對(duì)象的值進(jìn)行讀取,然后再寫入另外一個(gè)。對(duì)于兩個(gè)獨(dú)立的對(duì)象,這里就有兩個(gè)獨(dú)立的操作了,合并這兩個(gè)操作必定是不原子的。因此,操作就不被允許。

有限的特性集使得std::atomic_flag非常適合于作自旋互斥鎖。初始化標(biāo)志是“清除”,并且互斥量處于解鎖狀態(tài)。為了鎖上互斥量,循環(huán)運(yùn)行test_and_set()直到舊值為false,就意味著這個(gè)線程已經(jīng)被設(shè)置為true了。解鎖互斥量是一件很簡(jiǎn)單的事情,將標(biāo)志清除即可。實(shí)現(xiàn)如下面的程序清單所示:

清單5.1 使用std::atomic_flag實(shí)現(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()中等待,所以這里幾乎不可能有競(jìng)爭(zhēng)的存在,并且可以確?;コ?。當(dāng)我們看到內(nèi)存順序語義時(shí),你將會(huì)看到它們是如何對(duì)一個(gè)互斥鎖保證必要的強(qiáng)制順序的。這個(gè)例子將在5.3.6節(jié)中展示。

由于std::atomic_flag局限性太強(qiáng),因?yàn)樗鼪]有非修改查詢操作,它甚至不能像普通的布爾標(biāo)志那樣使用。所以,你最好使用std::atomic<bool>,接下來讓我們看看應(yīng)該如何使用它。

5.2.3 std::atomic的相關(guān)操作

最基本的原子整型類型就是std::atomic<bool>。如你所料,它有著比std::atomic_flag更加齊全的布爾標(biāo)志特性。雖然它依舊不能拷貝構(gòu)造和拷貝賦值,但是你可以使用一個(gè)非原子的bool類型構(gòu)造它,所以它可以被初始化為true或false,并且你也可以從一個(gè)非原子bool變量賦值給std::atomic<bool>的實(shí)例:

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

另一件需要注意的事情時(shí),非原子bool類型的賦值操作不同于通常的操作(轉(zhuǎn)換成對(duì)應(yīng)類型的引用,再賦給對(duì)應(yīng)的對(duì)象):它返回一個(gè)bool值來代替指定對(duì)象。這是在原子類型中,另一種常見的模式:賦值操作通過返回值(返回相關(guān)的非原子類型)完成,而非返回引用。如果一個(gè)原子變量的引用被返回了,任何依賴與這個(gè)賦值結(jié)果的代碼都需要顯式加載這個(gè)值,潛在的問題是,結(jié)果可能會(huì)被另外的線程所修改。通過使用返回非原子值進(jìn)行賦值的方式,你可以避免這些多余的加載過程,并且得到的值就是實(shí)際存儲(chǔ)的值。

雖然有內(nèi)存順序語義指定,但是使用store()去寫入(true或false)還是好于std::atomic_flag中限制性很強(qiáng)的clear()。同樣的,test_and_set()函數(shù)也可以被更加通用的exchange()成員函數(shù)所替換,exchange()成員函數(shù)允許你使用你新選的值替換已存儲(chǔ)的值,并且自動(dòng)的檢索原始值。std::atomic<bool>也支持對(duì)值的普通(不可修改)查找,其會(huì)將對(duì)象隱式的轉(zhuǎn)換為一個(gè)普通的bool值,或顯示的調(diào)用load()來完成。如你預(yù)期,store()是一個(gè)存儲(chǔ)操作,而load()是一個(gè)加載操作。exchange()是一個(gè)“讀-改-寫”操作:

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(),不僅僅是一個(gè)“讀-改-寫”的操作;它還介紹了一種新的存儲(chǔ)方式:當(dāng)當(dāng)前值與預(yù)期值一致時(shí),存儲(chǔ)新值的操作。

存儲(chǔ)一個(gè)新值(或舊值)取決于當(dāng)前值

這是一種新型操作,叫做“比較/交換”,它的形式表現(xiàn)為compare_exchange_weak()和compare_exchange_strong()成員函數(shù)?!氨容^/交換”操作是原子類型編程的基石;它比較原子變量的當(dāng)前值和一個(gè)期望值,當(dāng)兩值相等時(shí),存儲(chǔ)提供值。當(dāng)兩值不等,期望值就會(huì)被更新為原子變量中的值?!氨容^/交換”函數(shù)值是一個(gè)bool變量,當(dāng)返回true時(shí)執(zhí)行存儲(chǔ)操作,當(dāng)false則更新期望值。

對(duì)于compare_exchange_weak()函數(shù),當(dāng)原始值與預(yù)期值一致時(shí),存儲(chǔ)也可能會(huì)不成功;在這個(gè)例子中變量的值不會(huì)發(fā)生改變,并且compare_exchange_weak()的返回是false。這可能發(fā)生在缺少獨(dú)立“比較-交換”指令的機(jī)器上,當(dāng)處理器不能保證這個(gè)操作能夠自動(dòng)的完成——可能是因?yàn)榫€程的操作將指令隊(duì)列從中間關(guān)閉,并且另一個(gè)線程安排的指令將會(huì)被操作系統(tǒng)所替換(這里線程數(shù)多于處理器數(shù)量)。這被稱為“偽失敗”(spurious failure),因?yàn)樵斐蛇@種情況的原因是時(shí)間,而不是變量值。

因?yàn)閏ompare_exchange_weak()可以“偽失敗”,所以這里通常使用一個(gè)循環(huán):

bool expected=false;
extern atomic<bool> b; // 設(shè)置些什么
while(!b.compare_exchange_weak(expected,true) && !expected);

在這個(gè)例子中,循環(huán)中expected的值始終是false,表示compare_exchange_weak()會(huì)莫名的失敗。

另一方面,如果實(shí)際值與期望值不符,compare_exchange_strong()就能保證值返回false。這就能消除對(duì)循環(huán)的需要,就可以知道是否成功的改變了一個(gè)變量,或已讓另一個(gè)線程完成。

如果你想要改變變量值,且無論初始值是什么(可能是根據(jù)當(dāng)前值更新了的值),更新后的期望值將會(huì)變更有用;經(jīng)歷每次循環(huán)的時(shí)候,期望值都會(huì)重新加載,所以當(dāng)沒有其他線程同時(shí)修改期望時(shí),循環(huán)中對(duì)compare_exchange_weak()或compare_exchange_strong()的調(diào)用都會(huì)在下一次(第二次)成功。如果值的計(jì)算很容易存儲(chǔ),那么使用compare_exchange_weak()能更好的避免一個(gè)雙重循環(huán)的執(zhí)行,即使compare_exchange_weak()可能會(huì)“偽失敗”(因此compare_exchange_strong()包含一個(gè)循環(huán))。另一方面,如果值計(jì)算的存儲(chǔ)本身是耗時(shí)的,那么當(dāng)期望值不變時(shí),使用compare_exchange_strong()可以避免對(duì)值的重復(fù)計(jì)算。對(duì)于std::atomic<bool>這些都不重要——畢竟只可能有兩種值——但是對(duì)于其他的原子類型就有較大的影響了。

“比較/交換”函數(shù)很少對(duì)兩個(gè)擁有內(nèi)存順序的參數(shù)進(jìn)行操作,這就就允許內(nèi)存順序語義在成功和失敗的例子中有所不同;其可能是對(duì)memory_order_acq_rel語義的一次成功調(diào)用,而對(duì)memory_order_relaxed語義的一次失敗的調(diào)動(dòng)。一次失敗的“比較/交換”將不會(huì)進(jìn)行存儲(chǔ),所以“比較/交換”操作不能擁有memeory_order_release或memory_order_acq_rel語義。因此,這里不保證提供的這些值能作為失敗的順序。你也不能提供比成功順序更加嚴(yán)格的失敗內(nèi)存順序;當(dāng)你需要memory_order_acquire或memory_order_seq_cst作為失敗語序,那必須要如同“指定它們是成功語序”那樣去做。

如果你沒有指定失敗的語序,那就假設(shè)和成功的順序是一樣的,除了release部分的順序:memory_order_release變成memory_order_relaxed,并且memoyr_order_acq_rel變成memory_order_acquire。如果你都不指定,他們默認(rèn)順序?qū)閙emory_order_seq_cst,這個(gè)順序提供了對(duì)成功和失敗的全排序。下面對(duì)compare_exchange_weak()的兩次調(diào)用是等價(jià)的:

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é)中會(huì)詳解對(duì)于不同內(nèi)存順序選擇的結(jié)果。

std::atomic<bool>std::atomic_flag的不同之處在于,std::atomic<bool>不是無鎖的;為了保證操作的原子性,其實(shí)現(xiàn)中需要一個(gè)內(nèi)置的互斥量。當(dāng)處于特殊情況時(shí),你可以使用is_lock_free()成員函數(shù),去檢查std::atomic<bool>上的操作是否無鎖。這是另一個(gè),除了std::atomic_flag之外,所有原子類型都擁有的特征。

第二簡(jiǎn)單的原子類型就是特化原子指針——std::atomic<T*>,接下來就看看它是如何工作的吧。

5.2.4 std::atomic<T*>:指針運(yùn)算

原子指針類型,可以使用內(nèi)置類型或自定義類型T,通過特化std::atomic<T*>進(jìn)行定義,就如同使用bool類型定義std::atomic<bool>類型一樣。雖然接口幾乎一致,但是它的操作是對(duì)于相關(guān)的類型的指針,而非bool值本身。就像std::atomic<bool>,雖然它既不能拷貝構(gòu)造,也不能拷貝賦值,但是他可以通過合適的類型指針進(jìn)行構(gòu)造和賦值。如同成員函數(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*>為指針運(yùn)算提供新的操作。基本操作有fetch_add()和fetch_sub()提供,它們?cè)诖鎯?chǔ)地址上做原子加法和減法,為+=, -=, ++和--提供簡(jiǎn)易的封裝。對(duì)于內(nèi)置類型的操作,如你所預(yù)期:如果x是std::atomic<Foo*>類型的數(shù)組的首地址,然后x+=3讓其偏移到第四個(gè)元素的地址,并且返回一個(gè)普通的Foo*類型值,這個(gè)指針值是指向數(shù)組中第四個(gè)元素。fetch_add()和fetch_sub()的返回值略有不同(所以x.ftech_add(3)讓x指向第四個(gè)元素,并且函數(shù)返回指向第一個(gè)元素的地址)。這種操作也被稱為“交換-相加”,并且這是一個(gè)原子的“讀-改-寫”操作,如同exchange()和compare_exchange_weak()/compare_exchange_strong()一樣。正像其他操作那樣,返回值是一個(gè)普通的T*值,而非是std::atomic<T*>對(duì)象的引用,所以調(diào)用代碼可以基于之前的值進(jìn)行操作:

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

因?yàn)閒etch_add()和fetch_sub()都是“讀-改-寫”操作,它們可以擁有任意的內(nèi)存順序標(biāo)簽,以及加入到一個(gè)釋放序列中。指定的語序不可能是操作符的形式,因?yàn)闆]辦法提供必要的信息:這些形式都具有memory_order_seq_cst語義。

剩下的原子類型基本上都差不多:它們都是整型原子類型,并且都擁有同樣的接口(除了相關(guān)的內(nèi)置類型不一樣)。下面我們就看看這一類類型。

5.2.5 標(biāo)準(zhǔn)的原子整型的相關(guān)操作

如同普通的操作集合一樣(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(),還有復(fù)合賦值方式((+=, -=, &=, |=和^=),以及++和--(++x, x++, --x和x--)。雖然對(duì)于普通的整型來說,這些復(fù)合賦值方式還不完全,但也十分接近完整了:只有除法、乘法和移位操作不在其中。因?yàn)?,整型原子值通常用來作?jì)數(shù)器,或者是掩碼,所以以上操作的缺失顯得不是那么重要;如果需要,額外的操作可以將compare_exchange_weak()放入循環(huán)中完成。

對(duì)于std::atomic<T*>類型緊密相關(guān)的兩個(gè)函數(shù)就是fetch_add()和fetch_sub();函數(shù)原子化操作,并且返回舊值,而符合賦值運(yùn)算會(huì)返回新值。前綴加減和后綴加減與普通用法一樣:++x對(duì)變量進(jìn)行自加,并且返回新值;而x++對(duì)變量自加,返回舊值。正如你預(yù)期的那樣,在這兩個(gè)例子中,結(jié)果都是相關(guān)整型的一個(gè)值。

我們已經(jīng)看過所有基本原子類型;剩下的就是std::atomic<>類型模板,而非其特化類型。那么接下來讓我們來了解一下std::atomic<>類型模板。

5.2.6 std::atomic<>主要類的模板

主模板的存在,在除了標(biāo)準(zhǔn)原子類型之外,允許用戶使用自定義類型創(chuàng)建一個(gè)原子變量。不是任何自定義類型都可以使用std::atomic<>的:需要滿足一定的標(biāo)準(zhǔn)才行。為了使用std::atomic<UDT>(UDT是用戶定義類型),這個(gè)類型必須有拷貝賦值運(yùn)算符。這就意味著這個(gè)類型不能有任何虛函數(shù)或虛基類,以及必須使用編譯器創(chuàng)建的拷貝賦值操作。不僅僅是這些,自定義類型中所有的基類和非靜態(tài)數(shù)據(jù)成員也都需要支持拷貝賦值操作。這(基本上)就允許編譯器使用memcpy(),或賦值操作的等價(jià)操作,因?yàn)樗鼈兊膶?shí)現(xiàn)中沒有用戶代碼。

最后,這個(gè)類型必須是“位可比的”(bitwise equality comparable)。這與對(duì)賦值的要求差不多;你不僅需要確定,一個(gè)UDT類型對(duì)象可以使用memcpy()進(jìn)行拷貝,還要確定其對(duì)象可以使用memcmp()對(duì)位進(jìn)行比較。之所以要求這么多,是為了保證“比較/交換”操作能正常的工作。

以上嚴(yán)格的限制都是依據(jù)第3章中的一個(gè)建議:不要將鎖定區(qū)域內(nèi)的數(shù)據(jù),以引用或指針的形式,作為參數(shù)傳遞給用戶提供的函數(shù)。通常情況下,編譯器不會(huì)為std::atomic<UDT>類型生成無鎖代碼,所以它將對(duì)所有操作使用一個(gè)內(nèi)部鎖。如果用戶提供的拷貝賦值或比較操作被允許,那么這就需要傳遞保護(hù)數(shù)據(jù)的引用作為一個(gè)參數(shù),這就有悖于指導(dǎo)意見了。當(dāng)原子操作需要時(shí),運(yùn)行庫也可自由的使用單鎖,并且運(yùn)行庫允許用戶提供函數(shù)持有鎖,這樣就有可能產(chǎn)生死鎖(或因?yàn)樽鲆粋€(gè)比較操作,而阻塞了其他的線程)。最終,因?yàn)檫@些限制可以讓編譯器將用戶定義的類型看作為一組原始字節(jié),所以編譯器可以對(duì)std::atomic<UDT>直接使用原子指令(因此實(shí)例化一個(gè)特殊無鎖結(jié)構(gòu))。

注意,雖然使用std::atomic<float>std::atomic<double>(內(nèi)置浮點(diǎn)類型滿足使用memcpy和memcmp的標(biāo)準(zhǔn)),但是它們?cè)赾ompare_exchange_strong函數(shù)中的表現(xiàn)可能會(huì)令人驚訝。當(dāng)存儲(chǔ)的值與當(dāng)前值相等時(shí),這個(gè)操作也可能失敗,可能因?yàn)榕f值是一個(gè)不同方式的表達(dá)。這就不是對(duì)浮點(diǎn)數(shù)的原子計(jì)算操作了。在使用compare_exchange_strong函數(shù)的過程中,你可能會(huì)遇到相同的結(jié)果,如果你使用std::atomic<>特化一個(gè)用戶自定義類型,且這個(gè)類型定義了比較操作,而這個(gè)比較操作與memcmp又有不同——操作可能會(huì)失敗,因?yàn)閮蓚€(gè)相等的值擁有不同的表達(dá)方式。

如果你的UDT類型的大小如同(或小于)一個(gè)int或void*類型時(shí),大多數(shù)平臺(tái)將會(huì)對(duì)std::atomic<UDT>使用原子指令。有些平臺(tái)可能會(huì)對(duì)用戶自定義類型(兩倍于int或void*的大小)特化的std::atmic<>使用原子指令。這些平臺(tái)通常支持所謂的“雙字節(jié)比較和交換”(double-word-compare-and-swap,DWCAS)指令,這個(gè)指令與compare_exchange_xxx相關(guān)聯(lián)著。這種指令的支持,對(duì)于寫無鎖代碼是有很大的幫助,具體的內(nèi)容會(huì)在第7章討論。

以上的限制也意味著有些事情你不能做,比如,創(chuàng)建一個(gè)std::atomic<std::vector<int>>類型。這里不能使用包含有計(jì)數(shù)器,標(biāo)志指針和簡(jiǎn)單數(shù)組的類型,作為特化類型。雖然這不會(huì)導(dǎo)致任何問題,但是,越是復(fù)雜的數(shù)據(jù)結(jié)構(gòu),就有越多的操作要去做,而非只有賦值和比較。如果這種情況發(fā)生了,你最好使用std::mutex保證數(shù)據(jù)能被必要的操作所保護(hù),就像第3章描述的。

當(dāng)使用用戶定義類型T進(jìn)行實(shí)例化時(shí),std::atomic<T>的可用接口就只有: load(), store(), exchange(), compare_exchange_weak(), compare_exchange_strong()和賦值操作,以及向類型T轉(zhuǎn)換的操作。表5.3列舉了每一個(gè)原子類型所能使用的操作。

http://wiki.jikexueyuan.com/project/cplusplus-concurrency-action/images/chapter5/5-3-table.png" alt="" />

表5.3 每一個(gè)原子類型所能使用的操作

5.2.7 原子操作的釋放函數(shù)

直到現(xiàn)在,我都還沒有去描述成員函數(shù)對(duì)原子類型操作的形式。但是,在不同的原子類型中也有等價(jià)的非成員函數(shù)存在。大多數(shù)非成員函數(shù)的命名與對(duì)應(yīng)成員函數(shù)有關(guān),但是需要“atomic_”作為前綴(比如,std::atomic_load())。這些函數(shù)都會(huì)被不同的原子類型所重載。在指定一個(gè)內(nèi)存序列標(biāo)簽時(shí),他們會(huì)分成兩種:一種沒有標(biāo)簽,另一種將“_explicit”作為后綴,并且需要一個(gè)額外的參數(shù),或?qū)?nèi)存順序作為標(biāo)簽,亦或只有標(biāo)簽(例如,std::atomic_store(&atomic_var,new_value)std::atomic_store_explicit(&atomic_var,new_value,std::memory_order_release)。不過,原子對(duì)象被成員函數(shù)隱式引用,所有釋放函數(shù)都持有一個(gè)指向原子對(duì)象的指針(作為第一個(gè)參數(shù))。

例如,std::atomic_is_lock_free()只有一種類型(雖然會(huì)被其他類型所重載),并且對(duì)于同一個(gè)對(duì)象a,std::atomic_is_lock_free(&a)返回值與a.is_lock_free()相同。同樣的,std::atomic_load(&a)和a.load()的作用一樣,但需要注意的是,與a.load(std::memory_order_acquire)等價(jià)的操作是std::atomic_load_explicit(&a, std::memory_order_acquire)。

釋放函數(shù)的設(shè)計(jì)是為了要與C語言兼容,在C中只能使用指針,而不能使用引用。例如,compare_exchange_weak()和compare_exchange_strong()成員函數(shù)的第一個(gè)參數(shù)(期望值)是一個(gè)引用,而std::atomic_compare_exchange_weak()(第一個(gè)參數(shù)是指向?qū)ο蟮闹羔?的第二個(gè)參數(shù)是一個(gè)指針。std::atomic_compare_exchange_weak_explicit()也需要指定成功和失敗的內(nèi)存序列,而“比較/交換”成員函數(shù)都有一個(gè)單內(nèi)存序列形式(默認(rèn)是std::memory_order_seq_cst),重載函數(shù)可以分別獲取成功和失敗內(nèi)存序列。

對(duì)std::atomic_flag的操作是“反潮流”的,在那些操作中它們“標(biāo)志”的名稱為: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++標(biāo)準(zhǔn)庫也對(duì)在一個(gè)原子類型中的std::shared_ptr<>智能指針類型提供釋放函數(shù)。這打破了“只有原子類型,才能提供原子操作”的原則,這里std::shared_ptr<>肯定不是原子類型。但是,C++標(biāo)準(zhǔn)委員會(huì)感覺對(duì)此提供額外的函數(shù)是很重要的??墒褂玫脑硬僮饔校簂oad, store, exchange和compare/exchange,這些操作重載了標(biāo)準(zhǔn)原子類型的操作,并且獲取一個(gè)std::shared_ptr<>*作為第一個(gè)參數(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ù)可以用來確定實(shí)現(xiàn)是否使用鎖,來保證原子性。

如之前的描述,標(biāo)準(zhǔn)原子類型不僅僅是為了避免數(shù)據(jù)競(jìng)爭(zhēng)所造成的未定義操作,它們還允許用戶對(duì)不同線程上的操作進(jìn)行強(qiáng)制排序。這種強(qiáng)制排序是數(shù)據(jù)保護(hù)和同步操作的基礎(chǔ),例如,std::mutexstd::future<>。所以,讓我繼續(xù)了解本章的真實(shí)意義:內(nèi)存模型在并發(fā)方面的細(xì)節(jié),如何使用原子操作同步數(shù)據(jù)和強(qiáng)制排序。