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)意
文章出處:樸實的一線攻城獅