內存模型一:什么是內存模型


縱然工作再忙也應該要留下自己思考的時間,這次我總結了一下對於內存模型的理解,起因是在公司聽了一場關於多線程編程的分享會。首先解釋一下,內存模型和對象模型是不同的。對象模型說的是一個對象是如何被設計的,其在內存中是如何布局的。而內存模型說的是,在多核多線程環境下,各種不同的CPU是如何以一種統一的方式來與內存交互的。

 

背景知識:CPU的高速緩存

總所周知,CPU和內存並不是直接交換數據的,它們之間還隔着一個高速緩存。高速緩存是對程序員透明的,這意味在編程的時候是感知不到CPU的緩存的存在的。一般情況下確實如此,但在,在某些特殊的情形下(多核多線程),就不能忽略緩存的存在了。這其實是和緩存的設計有關系,一般多處理器下的每個CPU都有一個自己的緩存,存儲在這個緩存的數據是其它CPU是無法查看的。

 

引入問題1:內存可見性

問題來了,由於緩存是每個CPU私有的,那么在多線程環境下,某個CPU修改了變量x后保存在本地緩存,對於其它CPU,何時才能發現變量x被修改呢?如何保證其它CPU的緩存中持有的x的值是最新的呢?

由此可見,在多核多線程環境下,讀寫共享變量要解決的不僅是原子性,還需要保證其內存可見性。更糟的是,現代CPU通常在執行指令時會允許一定程度上的亂序,這使保證在多個CPU緩存的數據一致更是增加了復雜性。通常方法是通過一個協議來保證數據在各個CPU的緩存是一致性,這就是緩存一致性協議。

關於緩存一致性簡單的舉個列子。CPU-0嘗試STORE(更新)變量x,但其發現其它CPU的緩存也持有這個x的copy(x此時為Shared狀態,非單個CPU獨占),那么當CPU-0在STORE之前,必須通過一個disable消息,告訴其它CPU所持有的變量x已經為臟數據,是不可用狀態。其它CPU在收到這個disable消息后必須回應CPU-0一個ack消息,這時候CPU-0才能開始STORE變量x。

通過緩存一致性協議之后,內存可見性問題似乎是得以解決了。但是,這里面還隱藏着另外一個問題:指令亂序!

 

引入問題2:亂序(memory reorder)

先來解釋一下,亂序,指的是程序指令實際上執行的順序,和我們書寫的指令的順序不一致。亂序分兩種,分別是編譯器的指令重排和CPU的亂序執行。本意上亂序是為了優化指令執行的速度而產生的。並且為了維護程序原來的語義,編譯器和CPU不會對兩個有數據依賴的指令重排(reorder)。這種保護在單線程的環境下是可以工作的,但是到了多線程,問題就復雜了。

 

舉個例子,CPU-0將要執行兩條指令,分別是:

  1. STORE x
  2. LOAD y

當CPU-0執行指令1的時候,發現這個變量x的當前狀態為Shared,這意味着其它CPU也持有了x,因此根據緩存一致性協議,CPU-0在修改x之前必須通知其它CPU,直到收到來自其它CPU的ack才會執行真正的修改x。但是,事情沒有這么簡單。現代CPU緩存通常都有一個Store Buffer,其存在的目的是,先將要Store的變量記下來,注意此時並不真的執行Store操作,然后待時機合適的時候再執行實際的Store。有了這個Store Buffer,CPU-0在向其它CPU發出disable消息之后並不是干等着,而是轉而執行指令2(由於指令1和指令2在CPU-0看來並不存在數據依賴)。這樣做效率是有了,但是也帶來了問題。雖然我們在寫程序的時候,是先STORE x再執行LOAD y,但是實際上CPU卻是先LOAD y再STORE x,這個便是CPU亂序執行(reorder)的一種情況!

當你的程序要求指令1、2有邏輯上的先后順序時,CPU這樣的優化就是有問題的。但是,CPU並不知道指令之間蘊含着什么樣的邏輯順序,在你告訴它之前,它只是假設指令之間都沒有邏輯關聯,並且盡最大的努力優化執行速度。因此我們需要一種機制能告訴CPU:這段指令執行的順序是不可被重排的!做這種事的就是內存屏障(memory barrier)!

 

內存屏障

還是上面那個例子,如果不想指令1、2被CPU重排,程序應該這么寫:

  1. STORE x
  2. WMB (Write memory barrier)
  3. LOAD y

通過在STORE x之后加上這個寫內存屏障,就能保證在之后LOAD y指令不會被重排到STORE x之前了。

 

內存模型是什么

前面講了那么多,那么內存模型是什么呢?

首先,殘酷的現實就是每個CPU設計都是不同的,每個CPU對指令亂序的程度也是不一樣的。比較保守的如x86僅會對Store Load亂序,但是一些優化激進的CPU(PS的Power)會允許更多情況的亂序產生。如果目標是寫一個跨平台多線程的程序,那么勢必要了解每一個CPU的細節,來插入確切的、足夠的內存屏障來保證程序的正確性。這是多么的不科學啊!科學的做法應該是,我為一個抽象的機器寫一套抽象的程序,然后在不同的平台下讓編程語言、編譯器來生成合適的內存屏障。因此,我們有了內存模型的概念。不同平台下的實現差別被統一的內存模型所隱藏,只需要根據這個抽象的內存模型來編寫程序即可,這便是偉大的抽象...

因此,在C++11里有了內存模型的在之后,我們可以僅通過標准庫就實現出跨平台線程安全的lock free程序(這在C++11之前是做不到的,雖然Java早就有了內存模型)。

 

參考資料

  1. memory barriers a hardware view for software hackers


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM