前言
在之前我有寫一篇關於資料庫的ACID分享RDBMS資料庫基本原則
假如我們系統是一個多執行續高併發系統也要注意Atomic不然會造成資料會有Data Racing導致bug產生..
本文使用範例在我github上CAS_Lock有興趣的人可以參考.
沒有注意Atomic會導致的問題
我是我們公司擔任技術面試官之一,假如面試者說他實作過高併發系統。
我就會先問以下問題來辨別是否要更深入面試高併發系統相關問題.
下面Sample Code是這樣.
我用Task
假裝高併發,下面有一個Member
類別預設給100000元的Balance
有一個UpdateBalance
每呼叫一次就扣10元,我透過For
跑10000次
理論上我預期在跑完下面顯示Balance會員餘額是0 (因為 10000*10 = 100000).
1 | class Program |
但執行後蠻常會是不等於0!!
這時我會問面試者兩個問題
- 為什麼會不等於0
- 如果要解決你會怎麼解決這個問題?
如果知道的人可以跳到CAS部分,如果不知原因我下面會跟大家分享
為什麼會不等於0
這個問題牽扯到Thread是如何對於變數操作的,Thread在操作變數之前會把資料複製一份進Thread Context中在操作我們要步驟
所以在Balance -= 10;
這段程式碼會拆成下面動作
- 將執行個體變數中的值載入至register。
- 將載入值減10
- 將異動後值放回原本值的Memory。
因為以上步驟,假如同一時間有兩個不同的Thread取到的Balance都是1000,並個別對於Balance減10元,我們原本預期這是兩個操作(預期資料為980)
但因為取的瞬間都是1000-10=990把數值放回變數中就導致少扣一個10元…
概念如下圖.
知道原因了要解決就簡單了.
使用Lock解決
因為這段程式碼遇到Data Raceing在同一個時間有兩個不同的Thread對於資料競爭
如果要避免競爭lock是一個比較方便的方式,他可以保證一瞬間只有一個Thread(Session)來執行某段程式碼(通常會放在異動資料那部分)來保證Isolation.
下面是使用lock版本的程式碼,能看到我在Balance -= 10;
這一段使用lock來確保每一個瞬間只有一個Thread可以異動資料,其他的Thread需要blocking等他完成
1 | class Program |
使用lock之後能發現不管執行幾次資料都會如我們預期顯示.
使用lock執行概念圖如下.
在同一時間我們會把要執行程式 利用一個類似保護網的方式,確保同一時間只有一個Thread來做操作.
Thread2取得lock在操作Thread1就必須等待Thread2執行完,在取值=>改值..等等動作
只是使用lock會降低些許吞吐量(但資料正確性是最重要的),所以要注意使用lock範圍大小
CAS(compare and swap)提高效率
CAS是利用compare and swap來確保資料Atomic.
因為CAS可以取得數值記憶體空間來比較給值並且他也是一條CPU原語具有原子性 cpu 硬體同步原語
前面有說過在Balance -= 10;
這段程式碼會拆成下面動作
- 將執行個體變數中的值載入至register。
- 將載入值減10
- 將異動後值放回原本值的Memory。
1 | balance -= 10; |
會拆解成類似下面動作
1 | int temp = balance; |
假如在取balance(附值給temp)跟把值重新寫入balance中間有其他Thread來操作,就會造成所謂Data Racing,因為對於我們來說上面後兩部有不可分割性(Atomic).
而這時候我們就可以使用CAS算法來幫我們解決問題,在C#如果我們想要達成變數修改的Atomic可以透過Interlocked
類別
使用Interlocked提高效率
在C#中我們可以使用Interlocked這個類別
對於Int
,Long
相關操作都有封裝成method.
下面這段虛擬碼解釋balance -= 10;
CAS中類似下面效果
1 | //original |
主要是把compare value source and set這段包成一個不可分割區段達到Atomic.
> 我上面用ref
來表示從記憶體位置取得balance
原始資料
CompareExchange
:把值改成另一個數值 具有Atomic (比較並交換)Decrement
:把數值– 具有AtomicIncrement
:把數值++ 具有Atomic
除了上面我們還可以針對Reference Type做Atomic有興趣的人在自行了解
1 | class Program |
下圖是比較兩者執行時間還有併發計算數值,能發現數值兩個都正確計算出0,但Interlocked執行時間少於lock版本.
Interlocked效率會比較高是因為block會造成Thread的blocking等待浪費,但Interlocked核心概念是在這段話Atomic取得資料跟原職比較(如果資料還沒改就把值修改進Memory中)
所以效率就會比lock好很多
小結
在有些情境適合使用Lock(例如許多操作需要有一致的Atomic)就比較適合.
Interlocked適合用在對於數值的Atomic.
在多執行緒的世界中要顧慮的點真的很多,稍有不甚就會造成很多錯誤.
因為多執行緒有許多地方需要注意,不然執行效率會不如單執行緒.
我慢慢可以理解為什麼Redis,Node.js一開始要使用 single Thread 來運作了…
__此文作者__:Daniel Shih(石頭)
__此文地址__: https://isdaniel.github.io/high-concurrency-atomic-cas-algorithm/
__版權聲明__:本博客所有文章除特別聲明外,均採用 CC BY-NC-SA 3.0 TW 許可協議。轉載請註明出處!