併發程式設計1-併發基礎
目錄:
併發基本概念、併發的優勢與風險、CPU多級快取、MESI、亂序執行優化、Java記憶體模型
併發基本概念:
併發:同時擁有兩個或多個執行緒,如果程式在單核處理器上執行,多個執行緒將交替地換入或換出記憶體,這些執行緒是同時"存在"的。每個執行緒都將處於執行過程中的某個狀態,如果執行在多核處理器上,此時,程式中的每個執行緒都將分配到一個處理器核上,因此可以同時執行。
高併發:網際網路分散式系統架構設計中必須考慮的因素之一,通常指,通過設計保證系統能夠同時並行處理很多請求。
併發的優勢與風險:
優勢:
速度提升:同時處理多個請求,響應更快。複雜的操作可以分成多個程序同時進行
優質設計:程式設計在某些情況下更簡單,可以有更多選擇
資源利用:CPU能夠在等待IO的時候做一些其他的事情
風險:
安全性:多個執行緒共享資料時可能會產生與期望不符的結果
活躍性:某個操作無法繼續進行下去時,就會發生活躍性問題。比如死鎖、飢餓等問題
效能:執行緒過多會使得CPU頻繁切換,排程時間增多。同步機制也會消耗過多記憶體
CPU多級快取:
(1)主存、快取記憶體、CPU核心的關係
帶有告訴快取的CPU執行計算的流程?
1、程式以及資料被載入到主記憶體
2、指令和資料被載入到CPU的快取記憶體
3、CPU執行指令,把結果寫到快取記憶體
4、快取記憶體中的資料寫回主記憶體
為什麼需要CPU cache?
因此CPU的頻率太快,快到主存跟不上。在處理器時鐘週期內,CPU常常需要等待主存,浪費資源。所以cache的出現,是為了緩解CPU和記憶體之間速度的不匹配問題。
cache容量遠遠小於主存,因此出現cache miss在所難免,既然cache不能包含CPU所需要的所有資料,那其存在的意義是什麼呢?其意義即是區域性性原理。
A、時間區域性性:如果某個資料被訪問,那麼在不久的將來它很可能再次訪問。
B、空間區域性性:如果某個資料被訪問,那麼與它相鄰的資料很快也可能被訪問。
(2)快取一致性
快取記憶體出現不久後,主存和告訴快取之間的速度差異越來越大,因此一些系統可能採用的二級快取,甚至三級快取。
在多核CPU中,記憶體的資料會在多個核心中存在資料副本,某一個核心發生修改操作,就產生了資料不一致的問題,而一致性協議正是用於保證多個CPU cache之間快取共享資料的一致。
在MESI出現之前的解決方案是匯流排機制,這種方案效率很低,鎖住匯流排期間,其他CPU無法訪問記憶體。
CPU多級快取-快取一致性MESI
MESI為了保證多個CPU快取中共享資料的一致性,定義了cache line的四種狀態。而CPU對cache line的四種操作可能產生不一致的狀態,因此快取控制器監聽到本地操作和遠端操作時,需要對地址一致的cache line狀態進行一致性修改,從而保證資料在多個快取之間保持一致性。
四種狀態:
M:被修改(Modified)
該快取行只被快取在該CPU的快取中,並且是被修改過的,與主存中的資料不一致。該快取行中的記憶體需要在未來某個時間點(允許其他CPU讀取該主存中相應記憶體之前)寫回主存。當寫回主存後,該快取行狀態會變成獨享(exclusive)狀態。
E:獨享的(Exclusive)
該快取行只被快取在該CPU的快取中,它是未被修改過的,與主存中資料一致。該狀態可以在任何時刻當有其他CPU讀取該記憶體時變成共享狀態。同樣地,當CPU修改該快取行中內容時,該狀態可以變成Modified狀態。
S:共享的(Shared)
該狀態意味著該快取行可能被多個CPU快取,並且各個快取中的資料與主存資料一致,當有一個CPU修改該快取行,其他CPU中該快取行可以被作廢,變成無效狀態。
I:無效的(Invalid)
該快取是無效的(可能有其他CPU修改了該快取行)
四種操作:
Local read:讀本地快取中的資料
Local write:將資料寫到本地的快取中
Remote read:將記憶體的資料讀取過來
Remote write:將資料寫回到主存中
在一個多核的系統中,每一個核都有自己的快取來共享主存匯流排,每個CPU會發出讀寫請求,而快取的目的是為了減少CPU讀寫共享主存的次數。
四種操作與四種狀態的關係:
一個快取除了在I狀態之外都可以滿足CPU的讀請求
一個寫請求只有在M或E狀態下才能執行。如果處在S狀態,必須先將該快取中快取行變成I狀態。通常以廣播的方式執行。不允許多個CPU修改同一個快取行,即使修改該快取行不同的資料也是不允許的。
一個處於M狀態的快取行,必須監聽所有試圖讀快取行的操作,這種操作必須在快取將快取行寫回到主存,並將狀態變為S狀態之前被延遲執行。
一個處於S狀態的快取行,也必須監聽其他快取使該快取行無效或者獨享該快取行的請求並將快取行變成無效。
一個處於E狀態的快取行,要監聽其他快取讀快取中該快取行的操作,一旦有該快取行的操作,它就會變成S狀態。
對於M和E兩種狀態而言,資料總是精確的,和快取行的真正狀態是一致的,S狀態可能是非一致的,如果一個快取將處於S狀態的快取行作廢,另一個快取可能已經獨享了該快取行,但是該快取卻不會將快取行升為E狀態,因為其他快取不會廣播它們作廢掉該快取行的通知。由於快取並沒有儲存該快取行的copy數量,因此也沒辦法確定自己是否獨享了該快取行。
E更像一個投機性的優化,因為一個CPU想修改一個S狀態的快取行,匯流排事務需要將所有該快取行copy的值變成I狀態,但修改E狀態的快取,卻不需要匯流排事務。
CPU多級快取-亂序執行優化
處理器為了提高運算速度而做出違背程式碼原有順序的優化,單核處理器能夠保證處理器做出的優化不影響結果,但多核處理器會造成亂序,使最終結果錯誤。
併發之Java記憶體模型
該模型實際上是描述Java中各種變數(執行緒共享變數)的訪問規則,以及在JVM中將變數儲存到記憶體和從記憶體讀取變數這樣的底層細節。其規定了不同執行緒如何及何時可以看到其他執行緒寫入共享變數的值以及如何在必要時同步對共享變數的訪問。
(1)Java記憶體模型介紹
JVM中記憶體分配的兩個概念:
A、stack(棧)
特點:存取速度快,物件生命週期確定,資料大小確定
儲存資料:基本型別變數,物件引用
位置:快取、暫存器、寫快取區
B、heap(堆)
特點:存取速度慢、執行時動態分配大小,物件生命週期不確定,垃圾回收作用域
儲存資料:物件
位置:主記憶體、快取
理論上說所有的stack和heap都儲存在物理主記憶體中,但隨著CPU運算其資料的副本可能被快取或暫存器持有,持有的資料遵從一致性協議。儲存在堆上的物件可以被持有該物件引用的棧訪問,能訪問物件也就能訪問該物件中的成員變數。當兩個執行緒同時訪問一個物件時,每個執行緒都擁有該物件成員物件變數的私有拷貝。
(2)Java記憶體模型抽象
執行緒對共享記憶體的所有操作都必須在自己的工作記憶體中進行,不能直接從主記憶體中讀寫。
不同執行緒無法直接訪問其他執行緒工作記憶體中的變數,因此共享變數的值傳遞需要通過主記憶體完成。
併發問題的根源:
當A、B兩個執行緒同時訪問某個物件的成員變數X,當A操作變數a時會將a副本拷貝到執行緒A的工作記憶體中,當執行緒a未執行完畢時,執行緒B也要訪問變數a。對於兩個執行緒而言,操作的都是自己工作空間中的變數副本。兩個執行緒副本互不可見。如果A執行緒先完成了任務並將改變後的a寫回主存,那麼執行緒B的運算結束後寫回主記憶體的a就會覆蓋原來執行緒A的結果。造成執行緒A的任務丟失。為了保證程式的準確性,我們就需要在併發時新增額外的同步操作。
Java記憶體模型-記憶體間的八種同步操作
Java記憶體模型定義了8中操作來完成,主記憶體與工作記憶體之間的互動協議。它們都是原子操作(除了對long和double型別的變數)
lock(鎖定):作用於主記憶體中的變數,將它標記為一個執行緒獨享變數
unlock(解鎖):作用於主記憶體中的變數,解除變數的鎖定狀態,被解除鎖定狀態的變數才能被其他執行緒鎖定
read(讀取):作用於主記憶體的變數,它把一個變數的值從主記憶體傳輸到執行緒的工作記憶體中,以便隨後的load動作使用
load(載入):把read操作從主記憶體中得到的變數值放入工作記憶體的變數的副本中
user(使用):把工作記憶體中的一個變數的值傳給執行引擎,每當虛擬機器遇到一個使用到變數的指令時都會使用該指令
assign(賦值):作用於工作記憶體的變數,它把一個從執行引擎接收到的值賦給工作記憶體的變數,每當虛擬機器遇到一個給變數賦值的位元組碼指令時執行這個操作
store(儲存):作用於工作記憶體的變數,它把工作記憶體中一個變數的值傳送到主記憶體中,以便隨後的write操作使用
write(寫入):作用於主記憶體的變數,它把store操作從工作記憶體中得到的變數的值放入主記憶體的變數中
操作規則:
1、不允許read和load、store和write操作之一單獨出現
2、不允許一個執行緒丟棄它最近assign的操作,即變數在工作記憶體中改變了之後必須同步到主記憶體
3、不允許一個執行緒無原因地(沒發生過任何assign操作)把資料從工作記憶體同步回主記憶體
4、一個新的變數只能在主記憶體中誕生,不允許在工作記憶體中直接使用一個未被初始化(load或assign)的變數。執行user和store操作之前,必須先執行過assign和load操作
5、一個變數在同一時刻只允許一個執行緒對其進行lock,但lock操作可以被同一執行緒重複執行多次。必須執行相同次數的unlock才能解鎖。兩者必須成對出現
6、如果對一個變數執行lock,將會清空工作記憶體中此變數的值,在執行引擎使用這個變數前需要重新執行load或assign來初始化
7、如果一個變數事先沒有被lock操作鎖定,則不允許它執行unlock,也不允許去unlock一個被其他執行緒鎖定的變數
8、對一個變數執行unlock之前,必須先把此變數同步到主記憶體中(執行store和write)
9、如果要把一個變數從主記憶體中複製到工作記憶體中,就需要按順序執行read和load操作。從工作記憶體同步到主記憶體,就要按順序執行store和write操作。Java記憶體模型要求上述操作按順序執行,沒有保證必須是連續執行