從硬體級別再看可見性和有序性
前言
王子之前的文章對於併發程式設計中的可見性問題已經有了一個初步的介紹,總結出來就是CPU的快取會導致可見性問題。
這樣的解釋其實是沒有問題的,但這裡說的“快取”其實一個籠統的概念,快取其實指的是暫存器、快取記憶體和寫緩衝器。
今天我們就從硬體的級別再來探索一下出現可見性問題的原因,讓小夥伴們有一個更深的認識。
同時再深入探索一下有序性問題的產生原因。
如果小夥伴們對於暫存器、快取記憶體、緩衝器、匯流排的概念還不清楚,建議自行去查閱資料瞭解。
出現可見性問題的原因
首先,我們知道每個CPU都有自己的暫存器,它用於儲存臨時的二進位制資料,做一些資料運算。
所以當多個CPU各自執行一個執行緒的時候,就會導致在暫存器中對資料的修改對其他CPU是不可見的。
然後,一個CPU對變數的寫操作都是針對寫緩衝器的,並不是直接把值寫到主記憶體中。
所以沒有寫到主記憶體中的資料,對其他CPU是不可見的。
然後寫入緩衝器後,會把更新後的資料寫入到快取記憶體中,之後把變數更新資訊通過匯流排通知給其他CPU,但是其他CPU可能會認為這個更新是無效的,不會更新它自己的快取記憶體資料,這就導致了快取記憶體的可見性問題。
整體的記憶體模型如圖:
MESI協議解決可見性
解決可見性問題的一種方案就是MESI協議,這個MESI協議,根據不同的硬體系統,會有不同的實現方式。
比如MESI的一種實現方式,就是CPU接收到變數更新的訊息後,直接更新資料到自己的快取記憶體中,這樣各個CPU快取記憶體中的資料就一致了,解決了可見性問題。
說到MESI協議,王子要跟大家說兩個新名詞,flush和refresh。
先來說一下flush。
flush就是把自己更新的值重新整理到快取記憶體(或主記憶體)中,除了flush操作,同時還會發送一個訊息到匯流排(bus),通知其他處理器某個變數值被修改了。
那refresh又是什麼呢?
refresh指的是,處理器中的執行緒在讀取某個變數的時候,如果發現其他處理器的執行緒修改了這個變數,那就必須過期掉自己快取記憶體中的值,從其他處理器的快取記憶體(或主記憶體)中讀取變數,同步到自己的快取記憶體中。
這就是MESI協議最最基礎的原理。
探索有序性問題
之前的文章我們已經說過,指令重排會導致有序性問題,那麼具體什麼時候會發生指令重排呢?這就要從程式碼的編譯過程說起了。
首先我們寫的java程式碼會被javac靜態編譯器進行編譯,編譯成class位元組碼,然後會經過JIT動態編譯器編譯成作業系統可以執行的機器碼,在編譯的過程中,有一個編譯優化的概念,為了提高執行效率,可能會發生指令重排,例子就是之前文章中我們說到的double check單例模式,這裡就不再說明了。
除了編譯會發生指令重排,CPU本身也可能改變指令的執行順序,另外快取記憶體、寫緩衝器和無效佇列在硬體層面也可能會改變指令的順序。
接著我們來探索一下CPU是如何出現指令重排的?這就涉及到CPU的指令亂序和猜測執行機制了。
首先我們來看一下指令亂序機制。
CPU獲取到的指令是不一定能直接執行的,比如指令要執行網路通訊、磁碟IO、獲取鎖等,為了提升效率,CPU使用的就是指令亂序機制。
把編譯好的指令一條一條的讀取到處理器中,但哪條指令先就緒可以執行了,就會先執行,而不會去按照順序執行。
然後將指令執行後的結果放入指令重排序處理器中,重排序處理器再把這些結果按照最開始的指令順序同步到主記憶體或寫緩衝器中。
這就是指令亂序機制,可能出現有序性問題。
除此之外還有一個猜測執行機制,比如if判斷後,執行一堆程式碼,可能先去執行這堆程式碼,然後再進行判斷,如果判斷成立,就採納執行的結果,否則不採納執行的結果,這種機制也可能出現有序性問題。
說完了cpu,再來看看快取記憶體、寫緩衝器是如何導致記憶體重排序的。
首先來了解兩個概念,store和load。
store指的是處理器將資料寫入寫緩衝器這一過程,load指的是處理器從快取記憶體裡讀資料這一過程。這兩個過程可能導致記憶體重排序,一共有四種可能:
LoadLoad重排序:一個處理器先執行L1,後執行L2,另一個處理器可能看到的是先執行L2,後執行L1;
StoreStore重排序:一個處理器先執行W1,後執行W2,另一個處理器可能看到的是先執行W2,後執行W1;
LoadStore重排序:一個處理器先執行L1,後執行W2,另一個處理器可能看到的是先執行W2,後執行L1;
StoreLoad重排序:一個處理器先執行W1,後執行L2,另一個處理器可能看到的是先執行L2,後執行W1;
為了便於理解,以StoreStore重排序為例,假如快取記憶體按照W1W2的順序接收到兩個操作,為了提高效能,先執行了W2,後執行了W1,這就發生了記憶體重排序,其他處理器看到的順序就是重新排序後的順序。
總結
今天我們從硬體級別重新認識了一下可見性和有序性問題。
對於可見性,我們介紹了CPU的快取機制和MESI協議。
對於有序性,我們介紹了編譯優化、CPU的指令亂序和猜測執行、記憶體重排序機制導致的指令重排問題。
這就是全部內容了,希望小夥伴們能夠通過對底層原理的理解,更容易的理解併發程式設計。
往期文章推薦:
JVM專欄
訊息中介軟體專欄
併發程式設計專欄