Goroutine陷阱

Go在語言層面經過Goroutine與channel來支持併發編程,使併發編程看似變得異常簡單,但經過最近一段時間的編碼,愈來愈以爲簡單的東西,很容易會被濫用。Java的標準庫也讓多線程編程變得簡單,但想當初在公司定位Java的問題,發現不少的同窗因爲沒有深刻了解Java Thread的機制,Thread直接New從無論理複用,那Goroutine確定也要面臨這類的問題。程序員

1 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新手而言,可能會直接使用相似代碼,而不去思考它可能有問題。多線程

2 Goroutine Race問題

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問題。

相關文章
相關標籤/搜索