无锁数据结构二-乱序控制(栅栏)
內存柵欄
由于優化會導致對代碼的亂序執行,在并發執行時可能帶來問題。因此為了并行代碼的正確執行,我們需提示處理器對代碼優化做一些限制。而這些提示就是內存柵欄(memory barriers),用來對內存訪問進行管理。要詳細了解內存柵欄原理及產生原因,可參考無鎖數據結構(基礎篇):內存柵障。每種處理器架構都能提供一組完整的內存柵欄供開發使用,使用這些,我們能建立不同的內存模型。通過內存模型,我們能控制并發的執行順序,也即同步。
下面先對亂序進行一些介紹,可參考3,4
亂序
亂序分為編譯器亂序和處理器(cpu)亂序,下面是我對它們的一些了解。
編譯器亂序
處理器亂序
由于亂序可在不同層進行,柵欄也有多種,可分為編譯器內存柵欄(編譯器),內存柵欄(cpu)。對于c++11 來說其定義內存模型,std::memory_order對編譯器和cpu都會施加影響。下面主要介紹c++11的內存模型。
內存模型與執行順序
有如下三種內存模型:
可分成四種執行順序
所有這些內存模型定義在一個C++列表中– std::memory_order,包含以下六個常量:
- memory_order_seq_cst 指向序列一致性模型
- memory_order_acquire, memory_order_release,memory_order_acq_rel, memory_order_consume 指向基于獲取/釋放語義的模型
- memory_order_relaxed 指向寬松的內存序列化模型
其中:
- memory_order_relaxed 只保證此操作是原子的,不保證任何讀寫內存順序。
- memory_order_consume 對于當前線程依賴于當前原子變量A的變量的讀或寫不能重排到此位置前,其他線程對依賴于A的變量的在釋放A前(如store(memory_order_release))的讀寫,對于當前線程都是可見的(釋放/消費語義)。在大多數平臺上,這只影響到編譯器優化> 此語義作為一個“內存的禮物”被引入DECAlpha處理器中。
- memory_order_acquire 帶此內存順序的加載操作,在其影響的內存位置進行獲得操作:當前線程中讀或寫不能能被重排到此加載前。其他釋放同一原子變量的線程的所有寫入,為當前線程所可見(釋放/獲取語義)。與memory_order_consume區別:此標志影響所有變量讀寫,而consume影響依賴原子變量的讀寫。
- memory_order_release 帶此內存順序的存儲操作進行釋放操作:當前進程中的讀或寫不能被重排到此存儲后。當前線程的所有寫入,可見于獲得(acquire)該同一原子變量的其他線程(釋放/獲取語義),并且對該原子變量的帶依賴寫入變得對于其他消費同一原子對象的線程可見(釋放/消費語義)。
- memory_order_acq_rel 帶此內存順序的讀-修改-寫操作既是獲得操作又是釋放操作。當前線程的讀或寫內存不能被重排到此存儲前或后。所有釋放同一原子變量的線程的寫入可見于修改之前,而且修改可見于其他獲得同一原子變量的線程。
- memory_order_seq_cst 任何帶此內存順序的操作既是獲得操作又是釋放操作,加上存在一個單獨全序,其中所有線程以同一順序觀測到所有修改(序列一致)。
針對讀(加載),可選memory_order_acquire和 memory_order_consume。針對寫(存儲),僅能選memory_order_release。Memory_order_acq_rel是唯一可以用來做RMW運算,比如compare_exchange, exchange, fetch_xxx。事實上,因為RMW可以并發執行原子讀\寫,原子性RMW原語擁有獲取語義memory_order_acquire, 釋放語義memory_order_release 或者 memory_order_acq_rel.?
-memory_order_acq_rel – is somehow similar to memory_order_seq_cst, but RMW-operation is located inside the acquire/release-section -memory_order_relaxed – RMW-operation shifting (its load and store parts) upwards/downwards the code (for example, within the acquire/release section, if the operation is located inside such section) doesn’t lead to errors
c++11可通過下列語句組合實現內存模型
std::atomic::load syd::atomic::store ... ... std::atomic_thread_fence- 1
- 2
- 3
- 4
- 5
?
一般情況下上圖左右(store(memory_order_release)與atomic_thread_fence(memory_order_release))可以互相替換,但其仍有區別,Release Fence更加嚴格,其阻止下方的store穿過它,而Release Operation不行,如下,m_instance的store可在g_dummy.store上:
- 1
- 2
- 3
為簡單,在介紹模型時只介紹load/store
下面是我對各模型的理解
序列一致順序
這是一種嚴格的內存模型,它確保處理器按程序本身既定順序執行。
帶標簽 memory_order_seq_cst 的原子操作不僅以與釋放/獲得順序相同的方式排序內存(在一個線程中發生先于存儲的任何結果都變成做加載的線程中的可見副效應),還對所有擁有此標簽的內存操作建立一個單獨全序。
釋放獲取順序
同步僅建立在釋放和獲得同一原子對象的線程之間。其他線程可能看到與被同步線程的一者或兩者相異的內存訪問順序。?
在強順序系統( x86 、 SPARC TSO 、 IBM 主框架)上,釋放獲得順序對于多數操作是自動進行的。無需為此同步模式添加額外的 CPU 指令,只有某些編譯器優化受影響(例如,編譯器被禁止將非原子存儲移到原子存儲-釋放后,或將非原子加載移到原子加載-獲得前)。在弱順序系統( ARM 、 Itanium 、 Power PC )上,必須使用特別的 CPU 加載或內存柵欄指令。
通過下面代碼來說明此模型:
#include <thread> #include <atomic> #include <cassert> #include <vector>std::vector<int> data; std::atomic<int> flag = {0};void thread_1() {data.push_back(42);flag.store(1, std::memory_order_release);//memory_order_release 保證data.push_back(42)執行順序一定在store前 }void thread_2() {int expected=1;while (!flag.compare_exchange_strong(expected, 2, std::memory_order_acq_rel)) { //memory_order_acq_rel具有release和acquire,int expected=1;在前,expected = 1;在后//只有當thread_1的store執行后才會進入循環,此時能保證data==42,同時memory_order_acq_relexpected = 1; } }void thread_3() {while (flag.load(std::memory_order_acquire) < 2);//只有在執行了flag.compare_exchange_strong后,才跳出循環,此時已保證data==42assert(data.at(0) == 42); // 決不出錯 }int main() {std::thread a(thread_1);std::thread b(thread_2);std::thread c(thread_3);a.join(); b.join(); c.join(); }- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
釋放消費順序
使用memory_order_consume替換memory_order_acquire來提供比memory_order_acquire更弱的控制。
同步僅在釋放和消費同一原子對象的線程間建立。其他線程能見到與被同步線程的一者或兩者相異的內存訪問順序。
所有異于 DEC Alphi 的主流 CPU 上,依賴順序是自動的,無需為此同步模式產生附加的 CPU 指令,只有某些編譯器優化收益受影響(例如,編譯器被禁止牽涉到依賴鏈的對象上的推測性加載)。
注意當前(2015年2月)沒有產品編譯器跟蹤依賴鏈:消費操作被提升成獲得操作。
釋放消費順序的規范正在修訂中,而且暫時不鼓勵使用 memory_order_consume (C++17 起)
例子如下:
#include <thread> #include <atomic> #include <cassert> #include <string>std::atomic<std::string*> ptr; int data;void producer() {std::string* p = new std::string("Hello");data = 42;ptr.store(p, std::memory_order_release); }void consumer() {std::string* p2;while (!(p2 = ptr.load(std::memory_order_consume)));assert(*p2 == "Hello"); // 絕無出錯: *p2 從 ptr 攜帶依賴assert(data == 42); // 可能也可能不會出錯: data 不從 ptr 攜帶依賴,可能在ptr.load前執行。 }int main() {std::thread t1(producer);std::thread t2(consumer);t1.join(); t2.join(); }- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
此順序的典型使用情況,涉及對很少被寫入的數據結構(安排表、配置、安全策略、防火墻規則等)的共時讀取,和有指針中介發布的發布者-訂閱者情形,即當生產者發布消費者能通過其訪問信息的指針之時:無需令生產者寫入內存的所有其他內容對消費者可見(這在弱順序架構上可能是昂貴的操作)。這種場景的一個例子是 rcu 解引用.
寬松順序
提供最弱控制,只保證原子性
典型使用是計數器自增,例如std::shared_ptr 的引用計數器,因為這只要求原子性,但不要求順序或同步
參考文檔
總結
以上是生活随笔為你收集整理的无锁数据结构二-乱序控制(栅栏)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 无锁数据结构一
- 下一篇: selenium操作chrome时的一些