GO語言面試系列:(八)golang 并發(fā)安全性案例分析

golang 在1.5版本之前默認只使用一個核心來跑所有的goroutines,即GOMAXPROCS默認設置為1, ,即是串行執(zhí)行goroutines,在1.5版本后,GOMAXPROCS默認設置為當前計算機真實的核心線程數(shù),即是在并行執(zhí)行goroutines

并行執(zhí)行安全性案例分析

利用計算機多核處理的特性,并行執(zhí)行能成倍的提高程序的性能,但同時也帶入了數(shù)據(jù)安全性問題,下面看一個在線銀行轉(zhuǎn)賬的案例:

type User struct {
        Cash int
}

func (u *User) sendCash(to *User, amount int) bool {
    if u.Cash < amount {
        return false
    }
    /* 設置延遲Sleep,當多個goroutines并行執(zhí)行時,便于進行數(shù)據(jù)安全分析 */
    time.Sleep(500 * time.Millisecond)
    u.Cash = u.Cash - amount
    to.Cash = to.Cash + amount
    return true
}

func main() {
    me := User{Cash: 500}
    you := User{Cash: 500}
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        me.sendCash(&you, 50) //轉(zhuǎn)賬
        fmt.Fprintf(w, "I have $%d\n", me.Cash)
        fmt.Fprintf(w, "You have $%d\n", you.Cash)
        fmt.Fprintf(w, "Total transferred: $%d\n", (you.Cash - 500))
    })
    http.ListenAndServe(":8080", nil)
}

這是一個通用的Go Web應用,定義User數(shù)據(jù)結(jié)構(gòu),sendCash是在兩個User之間轉(zhuǎn)賬的服務,這里使用的是net/http 包,我們創(chuàng)建了一個簡單的Http服務器,然后將請求路由到轉(zhuǎn)賬50元的sendCash方法,在正常操作下,代碼會如我們預料一樣運行,每次轉(zhuǎn)移50美金,一旦一個用戶的賬戶余額達到0美金,就不能再進行轉(zhuǎn)出鈔票了,因為沒有錢了,但是,如果我們很快地發(fā)送很多請求,這個程序會繼續(xù)轉(zhuǎn)出很多錢,導致賬戶余額為負數(shù)。
這是課本上經(jīng)常談到的競爭情況race condition,在這個代碼中,賬戶余額的檢查是與從賬戶中取錢操作分離的,我們假想一下,如果一個請求剛剛完成賬戶余額檢查,但是還沒有取錢,也就是沒有減少賬戶余額數(shù)值;而另外一個請求線程同時也檢查賬戶余額,發(fā)現(xiàn)賬戶余額還沒有剩為零(結(jié)果兩個請求都一起取錢,導致賬戶余額為負數(shù)),這是典型的”check-then-act”競爭情況。這是很普遍存在的 并發(fā) bug。

用鎖解決竟態(tài)數(shù)據(jù)安全問題

那么我們?nèi)绾谓鉀Q呢?我們肯定不能移除檢查操作,而是確保檢查和取錢兩個動作之間沒有任何其他操作發(fā)生,其他語言是使用鎖,當賬戶進行更新時,鎖住禁止同時有其他線程操作,確保一次只有一個進程操作,也就是排斥鎖Mutex。,下面用golang自帶的sync包實現(xiàn)對轉(zhuǎn)賬判斷及數(shù)據(jù)操作過程的加鎖:

type User struct {
        Cash int
}

var transferLock *sync.Mutex

func (u *User) sendCash(to *User, amount int) bool {

    transferLock.Lock() //對轉(zhuǎn)賬操作進行加鎖
    defer transferLock.Unlock() //轉(zhuǎn)賬結(jié)束后解鎖釋放資源
    if u.Cash < amount {
        return false
    }
    /* 設置延遲Sleep,當多個goroutines并行執(zhí)行時,便于進行數(shù)據(jù)安全分析 */
    time.Sleep(500 * time.Millisecond)
    u.Cash = u.Cash - amount
    to.Cash = to.Cash + amount
    return true
}
func main() {
    me := User{Cash: 500}
    you := User{Cash: 500}
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        me.sendCash(&you, 50) //轉(zhuǎn)賬
        fmt.Fprintf(w, "I have $%d\n", me.Cash)
        fmt.Fprintf(w, "You have $%d\n", you.Cash)
        fmt.Fprintf(w, "Total transferred: $%d\n", (you.Cash - 500))
    })
    http.ListenAndServe(":8080", nil)
}

利用Channel,更好的實現(xiàn)并發(fā)

但是鎖的問題很顯然降低了程序并發(fā)的性能,鎖是并發(fā)設計的最大敵人,在Go中推薦使用通道Channel,能夠使用事件循環(huán)event loop機制更靈活地實現(xiàn)并發(fā);通過委托一個后臺協(xié)程監(jiān)聽通道,當通道中有數(shù)據(jù)時,立即進行轉(zhuǎn)賬操作,因為協(xié)程是順序地讀取通道中的數(shù)據(jù),也就是巧妙地回避了競爭情況,沒有必要使用任何狀態(tài)變量防止并發(fā)競爭了。 具體示例:

type User struct {
    Cash int
}
type Transfer struct {
    Sender    *User
    Recipient *User
    Amount    int
}

func sendCashHandler(transferchan chan Transfer) {
    var val Transfer
    for {
        val = <-transferchan
        val.Sender.sendCash(val.Recipient, val.Amount)
    }
}

func (u *User) sendCash(to *User, amount int) bool {
    if u.Cash < amount {
        return false
    }
    /* 設置延遲Sleep,當多個goroutines并行執(zhí)行時,便于進行數(shù)據(jù)安全分析 */
    time.Sleep(500 * time.Millisecond)
    u.Cash = u.Cash - amount
    to.Cash = to.Cash + amount
    return true
}

func main() {
    me := User{Cash: 500}
    you := User{Cash: 500}
    transferchan := make(chan Transfer)
    go sendCashHandler(transferchan)
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        transfer := Transfer{Sender: &me, Recipient: &you, Amount: 50}
        transferchan <- transfer
        fmt.Fprintf(w, "I have $%d\n", me.Cash)
        fmt.Fprintf(w, "You have $%d\n", you.Cash)
        fmt.Fprintf(w, "Total transferred: $%d\n", (you.Cash - 500))
    })
    http.ListenAndServe(":8080", nil)

上面這段代碼創(chuàng)建了比較可靠的系統(tǒng)從而避免了并發(fā)競爭,但是我們會帶來另外一個安全問題:DoS(Denial of Service服務拒絕),如果我們的轉(zhuǎn)賬操作慢下來,那么不斷進來的請求需要等待進行轉(zhuǎn)賬操作的那個協(xié)程從通道中讀取新數(shù)據(jù),但是這個線程忙于照顧轉(zhuǎn)賬操作,沒有閑功夫讀取通道中新數(shù)據(jù),這個情況會導致系統(tǒng)容易遭受DoS攻擊,外界只要發(fā)送大量請求就能讓系統(tǒng)停止響應。

祭出select 進一步提升性能

一些基礎機制比如buffered channel可以處理這種情況,但是buffered channel是有內(nèi)存上限的,不足夠保存所有請求數(shù)據(jù),優(yōu)化解決方案是使用Go杰出的select語句:

http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
    transfer := Transfer{Sender: &me, Recipient: &you, Amount: 50}
    /*轉(zhuǎn)賬*/
    result := make(chan int)
    go func(transferchan chan<- Transfer, transfer Transfer, result chan<- int) {
        transferchan <- transfer
        result <- 1
    }(transferchan, transfer, result)

    /*用select來處理超時機制*/
    select {
    case <-result:
        fmt.Fprintf(w, "I have $%d\n", me.Cash)
        fmt.Fprintf(w, "You have $%d\n", you.Cash)
        fmt.Fprintf(w, "Total transferred: $%d\n", (you.Cash - 500))
    case <-time.After(time.Second * 10): //超時處理
        fmt.Fprintf(w, "Your request has been received, but is processing slowly")
    }
})

這里提升了事件循環(huán),等待不能超過10秒,等待超過timeout時間,會返回一個消息給User告訴它們請求已經(jīng)接受,可能會花點時間處理,請耐心等候即可,使用這種方法我們降低了DoS攻擊可能,一個真正健壯的能夠并發(fā)處理轉(zhuǎn)賬且沒有使用任何鎖的系統(tǒng)誕生了。

小編微信:grey0805
DApp開源社區(qū),共享創(chuàng)意

文章出處:樸實的一線攻城獅

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容