Go在語言層面經過Goroutine與channel來支持併發編程,使併發編程看似變得異常簡單,但經過最近一段時間的編碼,愈來愈以爲簡單的東西,很容易會被濫用。Java的標準庫也讓多線程編程變得簡單,但想當初在公司定位Java的問題,發現不少的同窗因爲沒有深刻了解Java Thread的機制,Thread直接New從無論理複用,那Goroutine確定也要面臨這類的問題。程序員
Rob Pike在2012年的Google I/O大會上所作的「Go Concurrency Patterns」的演講上,說道過幾種基礎的併發模式。從一組目標中獲取第一個結果就是其中之一。golang
func First(query string, replicas ...Search) Result { c := make(chan Result) searchReplica := func(i int) { c <- replicas[i](query) } for i := range replicas { go searchReplica(i) } return <-c }
在First()函數中的結果channel是沒緩存的。這意味着只有第一個goroutine返回。其餘的goroutine會困在嘗試發送結果的過程當中,若是你有不止一個的重複時,每一個調用將會泄露資源。爲了不泄露,你須要確保全部的goroutine退出。一個不錯的方法是使用一個有足夠保存全部緩存結果的channel。編程
func First(query string, replicas ...Search) Result { c := make(chan Result,len(replicas)) searchReplica := func(i int) { c <- replicas[i](query) } for i := range replicas { go searchReplica(i) } return <-c }
另外一個不錯的解決方法是使用一個有default狀況的select語句和一個保存一個緩存結果的channel。default狀況保證了即便當結果channel沒法收到消息的狀況下,goroutine也不會堵塞。緩存
func First(query string, replicas ...Search) Result { c := make(chan Result,1) searchReplica := func(i int) { select { case c <- replicas[i](query): default: } } for i := range replicas { go searchReplica(i) } return <-c }
你也可使用特殊的取消channel來終止workers。安全
func First(query string, replicas ...Search) Result { c := make(chan Result) done := make(chan struct{}) defer close(done) searchReplica := func(i int) { select { case c <- replicas[i](query): case <- done: } } for i := range replicas { go searchReplica(i) } return <-c }
爲什麼在演講中會包含這些bug?Rob Pike僅僅是不想把演示覆雜化。這麼作是合理的,但對於Go新手而言,可能會直接使用相似代碼,而不去思考它可能有問題。多線程
Go語言支持函數中定義函數,看下一個例子:併發
func saveRequest(request *Request) { …. go func() { request.Users = []{1,2,3} … db.Save(request) } }
不少狀況下,因爲程序員對goroutine瞭解不夠深刻,又因爲goroutine使用很容易。爲了性能,很容易把一個同步函數變成異步函數,但這違背了go」不要經過共享內存來通訊,相反應該經過通訊來共享內存「的原則。即上述的例子中起了一個goroutine,並修改了request指針指向的對象。即便對request只讀,也可能不是安全,由於你沒法保證request指針不在其它goroutine中修改。dom
在本質上講,goroutine的使用會增長了函數的危險係數,尤爲是函數參數傳遞指針時。任何一個對象的操做,若是沒有加上鎖,當項目比較龐大時,可能不知道這個對象是否是會引發多個goroutine競爭。異步
什麼是goroutine race(競爭)問題?官網的文章 Introducing the Go Race Detect給出的例子以下:函數
package main import( "time" "fmt" "math/rand" ) func main() { start := time.Now() var t *time.Timer t = time.AfterFunc(randomDuration(), func() { fmt.Println(time.Now().Sub(start)) t.Reset(randomDuration()) }) time.Sleep(5 * time.Second) } func randomDuration() time.Duration { return time.Duration(rand.Int63n(1e9)) }
這個例子看起來沒任何問題,可是實際上,time.AfterFunc是會另外啓動一個goroutine來進行計時和執行func()。因爲func中有對t(Timer)進行操做(t.Reset),而主goroutine也有對t進行操做(t=time.After)。 這個時候,其實有可能會形成兩個goroutine對同一個變量進行競爭的狀況。
那什麼纔是goroutine的使用正確姿式,怎麼理解「經過通訊來共享內存」來避免Race問題?先看一個例子:
type SimpleAccount struct{ balance int } func NewSimpleAccount(balance int) *SimpleAccount { return &SimpleAccount{balance: balance} } func (acc *SimpleAccount) Deposit(amount uint) { acc.setBalance(acc.balance + int(amount)) } func (acc *SimpleAccount) Withdraw(amount uint) { if acc.balance >= int(amount) { acc.setBalance(acc.balance - int(amount)) } else { panic("傑克窮死") } } func (acc *SimpleAccount) Balance() int { return acc.balance } func (acc *SimpleAccount) setBalance(balance int) { acc.balance = balance } type ConcurrentAccount struct { account *SimpleAccount deposits chan uint withdrawals chan uint balances chan chan int } func NewConcurrentAccount(amount int) *ConcurrentAccount{ acc := &ConcurrentAccount{ account : &SimpleAccount{balance: amount}, deposits: make(chan uint), withdrawals: make(chan uint), balances: make(chan chan int), } acc.listen() return acc } func (acc *ConcurrentAccount) Balance() int { ch := make(chan int) acc.balances <- ch return <-ch } func (acc *ConcurrentAccount) Deposit(amount uint) { acc.deposits <- amount } func (acc *ConcurrentAccount) Withdraw(amount uint) { acc.withdrawals <- amount } func (acc *ConcurrentAccount) listen() { go func() { for { select { case amnt := <-acc.deposits: acc.account.Deposit(amnt) case amnt := <-acc.withdrawals: acc.account.Withdraw(amnt) case ch := <-acc.balances: ch <- acc.account.Balance() } } }() }
上面的例子,SimpleAccount全部方法,當多goroutine操做是不安全的,而經過ConcurrentAccount封裝,全部處理都統一經過channel通訊到listen開啓的goroutine,即只有一個goroutine能操做SimpleAccount中成員變量,那也就不會發現Goroutine Race問題。