深入理解Memory Order
深入理解Memory Order
- cpu 保證
- cache
- 編程技術
- lock-free
- wait-free
- Read–modify–write
- Compare-And-Swap(CAS)
- cas原理
- cas開銷
- test-and-set
- consensus member
- wait-free
- ABA problem
- memory order
- 背景知識
- 延伸:`__asm volatile("" ::: "memory")`的含義
- memory model
- C++ atomic
- memory_order
- memory_order_relaxed
- memory_order_consume
- memory_order_acquire
- memory_order_release
- memory_order_acq_rel
- memory_order_seq_cst
- 術語
- **Sequenced-before**
- Carries dependency
- Modification order
- Release sequence
- Synchronizes-with
- Dependency-ordered before
- **Inter-thread happens-before**
- Happens-before
- Visible side-effects
- 編程實現
- 操作組合完成原子操作
- fense
- atomic_thread_fence分類和效果
- fence和同樣memory order的原子操作同步效果的區別
- 利用fense進行同步
- 費曼方法
- linux kernel memory barriers閱讀筆記
- 我們為什么要學習原子操作和弱內存序
- Cacheline
- 參考鏈接
cpu 保證
intel SDM Volume 3 8.1.1 Guaranteed Atomic Operations談到了,在intel平臺了,有一些內存操作是被處理器天然的保證原自性,不需要lock
cache
編程技術
lock-free
Lock free允許單獨的線程個體阻塞,但是會保證系統整體上的吞吐,如果一個算法對應的程序的線程在運行了足夠長時間的情況下,至少有一個線程取得了進展,那么我們說這個算法是lock-free的。
概括起來就是,如果涉及到共享內存的多線程代碼在多線程執行下不可能互相影響導致被hang住,不管OS如何調度線程,至少有一個線程在做有用的事,那么就是lock-free。使用了鎖的代碼肯定不是lock free,因為一個線程加鎖后如果被系統切出去了其他所有線程都處于等待中。但是沒用鎖也不一定是lock free,**因為普通的代碼邏輯也可能會導致一個線程hang住另一個線程。**鎖之所以在高并發的時候表現很差,主要原因就是加鎖的線程會hang住其他待加鎖的線程,lock-free可以很好的解決這一問題。
wait-free
Read–modify–write
read modify write是一系列原子操作,比如 cas, fetchadd, test-and-set
Compare-And-Swap(CAS)
-
CAS是樂觀鎖并且可能失敗,讀寫鎖是悲觀鎖
-
類似c++11里的compare_exchange_strong
cas原理
-
比較再交換, CAS有3個操作數,內存值V,舊的預期值A,要修改的新值B。當且僅當預期值A和內存值V相同時,將內存值V修改為B,否則什么都不做。
-
如兩個線程去更改一個內存地址,則只會有一個成功。失敗的線程并不會被掛起,而是被告知這次競爭中失敗,并可以再次發起嘗試
-
從粒度上而言,cas只能保證一個共享變量的原子性,如果對多個共享變量操作,要加鎖了
cas開銷
CAS(比較并交換)是CPU指令級的操作,只有一步原子操作,所以非常快。但會有cache miss的開銷
- 當 CPU 從內存中讀取一個變量到它的寄存器中時,必須首先將包含了該變量的緩存線讀取到 CPU 高速緩存。同樣地,CPU 將寄存器中的一個值存儲到內存時,不僅必須將包含了該值的cacheline讀到 CPU 高速緩存,還必須確保沒有其他 CPU 擁有該cacheline的拷貝。
test-and-set
textandset就是傳入兩個值與一個內存地址,讓比較值與內存地址中的值相比較,如果一樣就交換,不一樣就不交換。返回值是內存地址中的值,整個過程是原子的。通常用于實現自旋鎖
consensus member
Maurice Herlihy (1991) ranks atomic operations by their consensus numbers, as follows:
- ∞: memory-to-memory move and swap, augmented queue, compare-and-swap, fetch-and-cons, sticky byte, load-link/store-conditional (LL/SC)[1]
- 2n - 2: n-register assignment
- 2: test-and-set, swap, fetch-and-add(FAA), queue, stack
- 1: atomic read and atomic write
從上面可以看出, cas是無窮,fetch and add 是2
wait-free
single-reader-single-writer queue
ABA problem
如果內存地址V初次讀取的值是A,并且在準備賦值的時候檢查到它的值仍然為A,那我們就能說它的值沒有被其他線程改變過了嗎?
如果在這段期間它的值曾經被改成了B,后來又被改回為A,那CAS操作就會誤認為它從來沒有被改變過。這個漏洞稱為CAS操作的“ABA”問題。Java并發包為了解決這個問題,提供了一個帶有標記的原子引用類“AtomicStampedReference”,它可以通過控制變量值的版本(加一個version字段)來保證CAS的正確性。因此,在使用CAS前要考慮清楚“ABA”問題是否會影響程序并發的正確性,如果需要解決ABA問題,改用傳統的互斥同步可能會比原子類更高效。
memory order
memory order受編譯器和cpu運行時重排的影響。c++ 11后也正式引入了memory order的概念,c++的memory oder更多則是一種約定和方法,給程序員提供了一種跨平臺的通用方法來限制上述兩種重排,這也極大地方便了我們實現lock free算法,在此之前,程序員如果想要限制重排,可能需要手動調用不同平臺的專有fence。
背景知識
- All writes to the same location are serialized, meaning they are observed in the same order by all observers, although some observers might not observe all of the writes.
- A read of a location does not return the value of a write until all observers observe that write.
延伸:__asm volatile("" ::: "memory")的含義
本節摘自朱元的知乎回答
memory model
對內存的操作可以概括為讀和寫,也就是load和store,因此reorder也就可以整體上分為以下四種類型:
X86/64(intel/AMD)屬于強memory model的范疇,只可能發生Storeload reorder。原來在讀操作之前的寫操作重排到之后,并且Intel 64 Architectures Software Developer’s Manual, Volume 3, section 8.2.2指出,Reads may be reordered with older writes to different locations but not with older writes to the same location.
C++ atomic
#include <atomic>如果使用atomic但是不用任何的成員函數,而是用自增,+=之類的操作符,那么相當于默認使用fetch_add,默認使用order:std::memory_order_seq_cst
memory_order
typedef enum memory_order {memory_order_relaxed,memory_order_consume,memory_order_acquire,memory_order_release,memory_order_acq_rel,memory_order_seq_cst } memory_order;memory_order_relaxed
這個很好理解,寬松的內存序,指定為這個的所有原子操作就真的僅僅是保證自身的原子性,不會對任何其他變量的讀寫產生影響。如果確實不需要其他的內存同步,那么這是最好的選擇,比如原子計數器。
memory_order_consume
memory_order_consume適用于load operation,對于采用此內存序的load operation,我們可以稱為consume operation,設有一個原子變量M上的consume operation,對周圍內存序的影響是:當前線程中該consume operation后的依賴該consume operation讀取的值的load 或strore不能被重排到該consume operation前,其他線程中所有對M的release operation及其之前的對數據依賴變量的寫入都對當前線程從該consume operation開始往后的操作可見,相比較于下面講的memory_order_acquire,memory_order_consume只是阻止了之后有依賴關系的重排。絕大部分平臺上,這個內存序只會影響到編譯器優化,依賴于dependency chain。但實際上很多編譯器都沒有正確地實現consume,導致等同于acquire。
memory_order_acquire
memory_order_acquire適用于load operation,對于采用此內存序的load operation,我們可以稱為acquire operation,設有一個原子變量M上的acquire operation,對周圍內存序的影響是:當前線程中該acquire operation后的load 或strore不能被重排到該acquire operation前,其他線程中所有對M的release operation及其之前的寫入都對當前線程從該acquire operation開始往后的操作可見。
memory_order_release
memory_order_release適用于store operation,對于采用此內存序的store operation,我們可以稱為release operation,設有一個原子變量M上的release operation,對周圍內存序的影響是:該release operation前的內存讀寫都不能重排到該release operation之后。并且:
- 截止到該release operation的所有內存寫入都對另外線程對M的acquire operation以及之后的內存操作可見,這就是release acquire 語義。
- 截止到該operation的所有M所依賴的內存寫入都對另外線程對M的consume operation以及之后的內存操作可見,這就是release consume語義。
memory_order_acq_rel
memory_order_acq_rel適用于read-modify-write operation,對于采用此內存序的read-modify-write operation,我們可以稱為acq_rel operation,既屬于acquire operation 也是release operation. 設有一個原子變量M上的acq_rel operation:自然的,該acq_rel operation之前的內存讀寫都不能重排到該acq_rel operation之后,該acq_rel operation之后的內存讀寫都不能重排到該acq_rel operation之前. 其他線程中所有對M的release operation及其之前的寫入都對當前線程從該acq_rel operation開始的操作可見,并且截止到該acq_rel operation的所有內存寫入都對另外線程對M的acquire operation以及之后的內存操作可見。
memory_order_seq_cst
memory_order_seq_cst 可以用于 load operation,release operation, read-modify-write operation三種操作,用于 load operation的時候有acquire operation的特性,用于 store operation的時候有release operation的特性, 用于 read-modify-write operation的時候有acq_rel operation的特性,除此之外,有個很重要的附加特性,一個單獨全序,也就是所有的線程會觀察到一致的內存修改,也就是順序一致性的強保證。參考鏈接17講到了memory_order_seq_cst和memory_order_acq_rel的不同
術語
C++ memory order循序漸進(二)—— C++ memory order基本定義和形式化描述所需術語關系詳解
熟悉了各種原子操作,這些術語就不難理解了
Sequenced-before
這個定義的是同一個線程內的一種根據表達式求值順序來的一種關系,完整的規則定義很復雜,可以參考http://en.cppreference.com/w/cpp/language/eval_order,其中最直觀常用的一條規則簡單來說如下:每一個完整表達式的值計算和副作用都Sequenced-before于下一個完整表達式的值計算和副作用。從而也就有以分號結束的語語句Sequenced-before于下一個以分號結束的語句,比如:
r2 = x.load(std::memory_order_relaxed); // C y.store(42, std::memory_order_relaxed); // D從而有 C Sequenced-before D。
Carries dependency
Modification order
Release sequence
Synchronizes-with
Dependency-ordered before
這個關系是針對consume來的,對于分屬不同線程的賦值A和賦值B,如果他們之間有以下關系之一:
我們就說 A dependency-ordered before B。
Inter-thread happens-before
Inter-thread happens-before,看字面意思就知道是定義了線程間賦值操作之間的關系,假如有線程1中的賦值操作A,線程2中的賦值操作B,如果滿足以下任意一條,那么有A inter-thread happens before B:
Happens-before
Visible side-effects
編程實現
要達到數據同步的效果,需要進行組合使用,我們可以在兩個地方指定memory order,一是atomic變量(原子變量)的操作,二是fence,利用fence可以不依賴原子變量進行同步
操作組合完成原子操作
仔細想想,原子操作其實存在兩個語義,一個是對當前線程限制重排,另一個是synchronized-with另外的線程
如圖,那種沒有synchronized-with的關系,不會有visble side-effect
fense
#include <atomic> extern "C" void atomic_thread_fence( std::memory_order order ) noexcept;fence不光可以不依賴原子操作進行同步,而且相比較于同樣memory order的原子操作,具有更強的內存同步效果。無論是純粹基于原子操作的同步,還是利用fence的,最常用的兩種就是release acquire和Sequentially-consistent ordering
atomic_thread_fence分類和效果
和atomic變量類似,atomic_thread_fence也可以指定六種memory order,指定不同memory order的fence可以分為以下幾類:
fence和同樣memory order的原子操作同步效果的區別
基于atomic_thread_fence(外加一個任意序的原子變量操作)的同步和基于原子操作的同步很類似,比如最常用的,都可以形成release acquire語義,但是從上面的描述可以看出,fence的效果要比基于原子變量的效果更強,在weak memory order平臺的開銷也更大。
以release為例,對于基于原子變量的release opration,僅僅是阻止前面的內存操作重排到該release opration之后,而release fence則是阻止重排到fence之后的任意store operation之后,比如一個簡單的例子:
std::string* p = new std::string("Hello"); ptr.store(p, std::memory_order_release);以下代碼具有同樣效果:
std::string* p = new std::string("Hello"); std::atomic_thread_fence(memory_order_release); ptr.store(p, std::memory_order_relaxed);再比如:
依賴ptr1的線程永遠能讀到正確值,但是依賴ptr2的不一定。
依賴ptr1和ptr2的的線程都永遠能讀到正確值
std::string* p = new std::string("Hello"); std::atomic_thread_fence(memory_order_release); ptr1.store(p, std::memory_order_relaxed); ptr2.store(p, std::memory_order_relaxed);利用fense進行同步
費曼方法
linux kernel memory barriers閱讀筆記
我們為什么要學習原子操作和弱內存序
本節摘自文章關于原子操作和弱內存序
有人說這些東西是不是太底層了,平時是不是用不到這些知識。其實不是,除了上面所說的無鎖編程場景之外,至少以下幾種場景還是要用到的。
Cacheline
沒有任何競爭或只被一個線程訪問的原子操作是比較快的,“競爭”指的是多個線程同時訪問同一個cacheline。現代CPU為了以低價格獲得高性能,大量使用了cache,并把cache分了多級。百度內常見的Intel E5-2620擁有32K的L1 dcache和icache,256K的L2 cache和15M的L3 cache。其中L1和L2 cache為每個核心獨有,L3則所有核心共享。一個核心寫入自己的L1 cache是極快的(4 cycles, ~2ns),但當另一個核心讀或寫同一處內存時,它得確認看到其他核心中對應的cacheline。對于軟件來說,這個過程是原子的,不能在中間穿插其他代碼,只能等待CPU完成一致性同步,這個復雜的硬件算法使得原子操作會變得很慢,在E5-2620上競爭激烈時fetch_add會耗費700納秒左右。訪問被多個線程頻繁共享的內存往往是比較慢的。比如像一些場景臨界區看著很小,但保護它的spinlock性能不佳,因為spinlock使用的exchange, fetch_add等指令必須等待最新的cacheline,看上去只有幾條指令,花費若干微秒并不奇怪。
要提高性能,就要避免讓CPU頻繁同步cacheline。這不單和原子指令本身的性能有關,還會影響到程序的整體性能。最有效的解決方法很直白:盡量避免共享。
- 一個依賴全局多生產者多消費者隊列(MPMC)的程序難有很好的多核擴展性,因為這個隊列的極限吞吐取決于同步cache的延時,而不是核心的個數。最好是用多個SPMC或多個MPSC隊列,甚至多個SPSC隊列代替,在源頭就規避掉競爭。
- 另一個例子是計數器,如果所有線程都頻繁修改一個計數器,性能就會很差,原因同樣在于不同的核心在不停地同步同一個cacheline。如果這個計數器只是用作打打日志之類的,那我們完全可以讓每個線程修改thread-local變量,在需要時再合并所有線程中的值,性能可能有幾十倍的差別。
一個相關的編程陷阱是false sharing:對那些不怎么被修改甚至只讀變量的訪問,由于同一個cacheline中的其他變量被頻繁修改,而不得不經常等待cacheline同步而顯著變慢了。多線程中的變量盡量按訪問規律排列,頻繁被其他線程修改的變量要放在獨立的cacheline中。要讓一個變量或結構體按cacheline對齊
參考鏈接
總結
以上是生活随笔為你收集整理的深入理解Memory Order的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 分布式文件系统HDFS解析
- 下一篇: Persistent Memory错误注