基於共享變量的併發

競爭條件
競爭條件指的是程序在多個goroutine交叉執行操做時,沒有給出正確的結果。競爭條件是很惡劣的一種場景,由於這種問題會一直潛伏在你的程序裏,而後在很是少見的時候蹦出來,或許只是會在很大的負載時纔會發生,又或許是會在使用了某一個編譯器、某一種平臺或者某一種架構的時候纔會出現。這些使得競爭條件帶來的問題很是難以復現並且難以分析診斷。
 
傳統上常常用經濟損失來爲競爭條件作比喻,因此咱們來看一個簡單的銀行帳戶程序。
示例1:一個銀行帳戶程序
// Package bank implements a bank with only one account.
package bank
var balance int
func Deposit(amount int) { balance = balance + amount }
func Balance() int { return balance }
兩個訪問者向同一個帳戶存錢
// Alice:
go func() {
       bank.Deposit(200)                // A1
       fmt.Println("=", bank.Balance()) // A2
}()

// Bob:
go bank.Deposit(100)                 // B
Bob的存款會在Alice存款操做中間,在餘額被讀到(balance + amount)以後,在餘額被更新以前(balance = ...),這樣會致使Bob的交易丟失。而這是由於Alice的存款操做A1其實是兩個操做的一個序列,讀取而後寫;能夠稱之爲A1r和A1w。下面是交叉時產生的問題:
Data race
0
A1r      0     ... = balance + amount
B      100
A1w    200     balance = ...
A2  "= 200"
在A1r以後,balance + amount會被計算爲200,因此這是A1w會寫入的值,並不受其它存款操做的干預。最終的餘額是$200。銀行的帳戶上的資產比Bob實際的資產多了$100。(譯註:由於丟失了Bob的存款操做,因此實際上是說Bob的錢丟了).
 
數據競爭 會在兩個以上的goroutine併發訪問相同的變量且至少其中一個爲寫操做時發生。 有以下3種方式避免數據競爭:

 第一種方法是不要去寫變量,只去讀變量。服務器

第二種方法,避免從多個goroutine訪問變量。架構

聊天服務器中的broadcaster goroutine是惟一一個可以訪問clients map的goroutine。這些變量都被限定在了一個單獨的goroutine中。併發

因爲其它的goroutine不可以直接訪問變量,它們只能使用一個channel來發送給指定的goroutine請求來查詢更新變量。這也就是Go的口頭禪「不要使用共享數據來通訊;使用通訊來共享數據」。一個提供對一個指定的變量經過cahnnel來請求的goroutine叫作這個變量的監控(monitor)goroutine。例如broadcaster goroutine會監控(monitor)clients map的所有訪問。dom

下面是一個重寫了的銀行的例子,這個例子中balance變量被限制在了monitor goroutine中,名爲teller:ide

// Package bank provides a concurrency-safe bank with one account.
package bank

var deposits = make(chan int) // send amount to deposit
var balances = make(chan int) // receive balance

func Deposit(amount int) { deposits <- amount }
func Balance() int       { return <-balances }

func teller() {
	var balance int // balance is confined to teller goroutine
	for {
		select {
		case amount := <-deposits:
			balance += amount
		case balances <- balance:
		}
	}
}

func init() {
	go teller() // start the monitor goroutine
}

串行綁定ui

即便當一個變量沒法在其整個生命週期內被綁定到一個獨立的goroutine,綁定依然是併發問題的一個解決方案。例如在一條流水線上的goroutine之間共享變量是很廣泛的行爲,在這二者間會經過channel來傳輸地址信息。若是流水線的每個階段都可以避免在將變量傳送到下一階段時再去訪問它,那麼對這個變量的全部訪問就是線性的。其效果是變量會被綁定到流水線的一個階段,傳送完以後被綁定到下一個,以此類推。這種規則有時被稱爲串行綁定。this

type Cake struct{ state string }

func baker(cooked chan<- *Cake) {
	for {
		cake := new(Cake)
		cake.state = "cooked"
		cooked <- cake // baker never touches this cake again
	}
}

func icer(iced chan<- *Cake, cooked <-chan *Cake) {
	for cake := range cooked {
		cake.state = "iced"
		iced <- cake // icer never touches this cake again
	}
}

第三種避免數據競爭的方法是容許不少goroutine去訪問變量,可是在同一個時刻最多隻有一個goroutine在訪問。這種方式被稱爲「互斥」,在下一節來討論這個主題。spa

相關文章
相關標籤/搜索