併發安全

併發安全

何爲併發安全,就是多個併發體在同一段時間內訪問同一個共享數據,共享數據能被正確處理。數據庫

併發不安全的後果

併發不安全最典型的案例就是賣票超售,設想有一家電影院,有兩個售票窗口,售票員售票時候先看一下當前剩餘票數是否大於0,若是大於0則售出票。
用僞代碼就是以下:安全

# 售票操做(一張票)
# 若是票數大於0
totalNum = getTotalNum()
if totalNum > 0
    # 則售出一張票
    totalNum = totalNum - 1
else
    failedToSold()

看上去也沒有什麼問題,流程圖以下:
jpg
此時票數剩下一張票,兩個售票窗口同時來了顧客,兩個售票人都看了一下剩餘票數還有一張,不約而同地收下顧客的錢,餘票還剩一張,可是卻售出了兩張票,就會出現致命的問題。
jpg併發

如何作到併發安全

目前最最主流的辦法就是加鎖就行操做,其實售票的整個操做同時間內只能一我的進行,在我看來歸根到底加鎖其實就是讓查詢和售票兩個步驟原子化,只能一塊執行,不能被其餘程序中斷,讓這步操做變成串行化。下面就介紹一下使查詢和售票原子化的常見程序操做:函數

鎖的作法就是每次進入這段變量共享的程序片斷,都要先獲取一下鎖,若是獲取成功則能夠繼續執行,若是獲取失敗則阻塞,直到其餘併發體把鎖給釋放,程序獲得執行調度才能夠執行下去。
鎖本質上就是讓併發體建立一個程序臨界區,臨界區一次只能進去一個併發體,僞代碼示意以下:性能

lock()
totalNum = getTotalNum()
if totalNum > 0
    # 則售出一張票
    totalNum = totalNum - 1
else
    failedToSold()
unlock()

順帶一提的是鎖能夠分爲寫鎖與排它鎖,通常如無特殊說明,通常鎖都是指寫鎖。atom

讀鎖與寫鎖

讀鎖也叫共享鎖,寫鎖也叫排它鎖,鎖的概念被髮明瞭以後,人們就想着若是我不少個併發體大部分時間都是讀,若是就把變量讀取的時候也要創建臨界區,那就有點太大題小作了。因而人們發明了讀鎖,一個臨界區若是加上了讀鎖,其餘併發體執行到相同的臨界區均可以加上讀鎖,執行下去,但不能加上寫鎖。這樣就保證了能夠多個併發體併發讀取而又不會互相干擾。code

隊列

隊列也是解決併發不安全的作法。多個併發體去獲取隊列裏的元素,而後進行處理,這種作法和上鎖其實大同小異,本質都是把併發的操做串行化,同一個數據同一個時刻只能交給一個併發體去處理。
僞代碼:隊列

# 第一個獲取到隊列的元素就能夠進行下去
isCanSold = canSoldList.pop()
totalNum = getTotalNum()
if totalNum > 0
    # 則售出一張票
    totalNum = totalNum - 1
else
    failedToSold()

CAS

CAS(compare and swap),先比對,而後再進行交換,和數據庫裏的樂觀鎖的作法很類似。事務

樂觀鎖

數據庫裏的樂觀鎖並非真的使用了鎖的機制,而是一種程序的實現思路。
樂觀鎖的想法是,每次拿取數據再去修改的時候很樂觀,認爲其餘人不會去修改這個數據,表另外維護一個額外版本號的字段。
查數據的時候記錄下該數據的版本號,若是成功修改的話,會修改該數據的版本號,若是修改的時候版本號和查詢的時候版本號不一致,則認爲數據已經被修改過,會從新嘗試查詢再次操做。
設咱們表有一個user表,除了必要的字段,還有一個字段version,表以下:get

id username money version
1 a 10 100
2 b 20 100

這時候咱們須要修改a的餘額-10元,執行事務語句以下:

while
    select @money = money, @version = version from user where username = a;
    if @money < 10
        print('餘額成功')
        break
    # 扣費前的預操做
    paied()
    # 實行扣費
    update user set money = money - 10, version = version + 1 where username = a and version = @version
    # 影響條數等於1,證實執行成功
    if @@ROWCOUNT == 1
        print('扣費成功')
        break
    else
        rollback
        print('扣費失敗,從新進行嘗試')

樂觀鎖的作法就是使用版本的形式,每次寫數據的時候會比對一下最開始的版本號,若是不一樣則證實有問題。
CAS的作法也是同樣的,在代碼裏面的實現稍有一點不一樣,因爲SQL每條語句都是原子性,查詢對應版本號的數據再更新的這個條件是原子性的。

update user set money = money - 10, version = version + 1 where username = a and version = @version

可是在代碼裏面兩條查詢和賦值兩個語句不是原子性的,須要有特定的函數讓cpu底層把兩個操做變成一個原子操做,在go裏面有atomic包支持實現,是這樣實現的:

for {
    user := getUserByName(A)
    version := user.version
    paied()
    if atomic.CompareAndSwapInt32(&user.version, version, version + 1) {
        user.money -= 10
    } else {
        rollback()
    }
}

atomic.CompareAndSwapInt32須要依次傳入要比較變量的地址,舊變量的值,修改後變量的值,函數會判斷舊變量的值是否與如今變量的地址是否相同,相同則把新變量的值寫入到該變量。 CAS的好處是不須要程序去建立臨界區,而是讓CPU去把兩個指令變成原子性操做,性能更好,可是若是變量會被頻繁更改的話,重試的次數變多反而會使得效率不如加鎖高。

相關文章
相關標籤/搜索