go語言學習筆記之並發編程

來源:互聯網
上載者:User

編譯自http://golang.org/doc/effective_go.html#concurrency (翻譯錯誤之處,敬請指正)

 

1. 通過通訊共用記憶體(Share by communicating):

  Do not communicate by sharing memory; instead, share memory by communicating.

  不要通過記憶體共用進行通訊;應當通過通訊來共用記憶體。使用通道(channels)來控制變數的訪問可以更為容易地編寫出清晰、正確的程式。

 

2. Goroutines:

  為什麼創造goroutine這個新詞? 原因就是現有的術語,比如線程、協程、進程等等都不能精確的表達其所要表達的內涵(譯者在這裡也建議不要將其翻譯成中文,因為中文裡也沒有任何詞可以精確的表示其內涵)。一個Goroutine就是一個與其它的goroutine在同一地址空間並存執行的函數,這句話有點繞口,但說明了兩個意思:一個goroutine就是一個函數;多個goroutine在同一地址空間並存執行。

  Goroutine是輕量的,比直接分配棧空間的方法要耗用少得多的記憶體。它起始棧(stack)很小,通過按需分配(和釋放)堆(heap)空間來增加記憶體使用量。
Goroutine可被多個OS線程複用,所以如果一個goroutine被阻滯(比如等待I/O時),其它的可以繼續運行。這種設計隱藏了線程建立和線程管理的複雜性。
通過在函數或方法前冠以關鍵詞go可以在一個新的goroutine中調用該函數。當調用完成後,該goroutine退出。(效果類似於Unix Shell中的放在命令後讓命令後台啟動並執行 &)。 

go list.Sort() // 並行運行list.Sort,不等待其結束。

 

  匿名函數在goroutine調用中也很有用。 

func Announce(message string, delay int64) {

  go func() {

    time.Sleep(delay)

    fmt.Println(message)

  }() //注意此處的括弧,必須調用該函數。
}

   在Go語言中,匿名函數是閉包(closure),其實現確保函數所引用的變數生存期與函數的生存期一樣長。

  這個例子不太實際,因為函數沒有在運行結束時發出訊號的方式。所以我們需要通道(channel)出場。

3. 通道(Channels)
  和map一樣,通道是參考型別,用make 分配記憶體。如果調用make時提供一個可選的整數參數,則該通道就會被分配相應大小的緩衝區。緩衝區大小預設為0,對應於無緩衝通道或者同步通道。 

ci := make(chan int) // 無緩衝整數通道
cj := make(chan int, 0) // 無緩衝整數通道
cs := make(chan *os.File, 100) // 緩衝的檔案指標通道

  通道將通訊(值的交換)與同步結合在一起,確保兩個計算過程(goroutine)都處於已知狀態。

  以前面那個後台並行排序為例。通道可用來讓正在啟動並執行goroutine等待排序完成。

c := make(chan int) // Allocate a channel.
// 在goroutine中啟動排序,當排序完成時,通道上發出訊號
go func() {
  list.Sort()
  c <- 1 // 發送一個訊號,值是多少無所謂。
}()
doSomethingForAWhile()
<-c // 等待排序完成,丟棄被發送的值。

 

  收信者(receivers)在收到資料前會一直被阻滯。如果通道是非緩衝的,則發信者(sender)在收信者接收到資料前也一直被阻滯。如果通道有緩衝區,發信者只有在資料被填入緩衝區前才被阻滯;如果緩衝區是滿的,意味著寄件者要等到某個收信者取走一個值。

  緩衝的通道可以象號誌一樣使用,比如用來限制輸送量。在下面的例子中,進入的請求被傳遞給handle,handle發送一個值到通道,接著處理請求,最後從通道接收一個值。通道緩衝區的大小限制了並發調用process的數目。

var sem = make(chan int, MaxOutstanding)
func handle(r *Request) {
  sem <- 1 // 等待隊列緩衝區非滿
  process(r) // 處理請求,可能會耗費較長時間.
  <-sem // 請求處理完成,準備處理下一個請求
}
func Serve(queue chan *Request) {
  for {
    req := <-queue
    go handle(req) //不等待handle完成
  }
}

 

  通過啟動固定數目的handle goroutines也可以實現同樣的功能,這些goroutines都從請求通道中讀取請求。Goroutines的數目限制了並發調用process的數目。Serve函數也從一個通道中接收退出訊號;在啟動goroutines後,它處於阻滯狀態,直到接收到退出訊號:

func handle(queue chan *Request) {
  for r := range queue {
   process(r)
  }
}

func Serve(clientRequests chan *clientRequests, quit chan bool) {
  // 啟動請求處理
  for i := 0; i < MaxOutstanding; i++ {
    go handle(clientRequests)
  }
  <-quit // 等待退出訊號
}

 

4. 通過通道傳遞通道(Channels of channels)

  Go最重要的特性之一就是: 通道是Go最重要的特性之一就是: 通道可以像其它類型的數值一樣被分配記憶體並傳遞。此特性常用於實現安全且並行的去複用(demultiplexing)。 

  前面的例子中,handle是一個理想化的處理請求的函數,但是我們沒有定義它所能處理的請求的具體類型。如果該類型包括了一個通道,每個用戶端就可以提供自己方式進行應答

type Request struct {
  args []int
  f func([]int) int
  resultChan chan int
}

 

  用戶端提供一個函數、該函數的參數以及一個請求對象用來接收應答的通道

func sum(a []int) (s int) {
for _, v := range a {
  s += v
}
return
}

request := &Request{[]int{3, 4, 5}, sum, make(chan int)}
// 發送請求
clientRequests <- request
// 等待響應.
fmt.Printf("answer: %d\n", <-request.resultChan)

  在伺服器端,處理請求的函數是

func handle(queue chan *Request) {
  for req := range queue {
    req.resultChan <- req.f(req.args)
  }
}

  顯然要使這個例子更為實際還有很多工作要做,但這是針對速度限制、並行、非阻滯RPC系統的架構,而且其中也看不到互斥(mutex)的使用。

 

5. 並行(Parallelization)

  這些思想的另一個應用是利用多核CPU進行並行計算。如果計算過程可以被分為多個片段,則它可以通過這樣一種方式被並行化:在每個片段完成後通過通道發送訊號。

  假設我們有一個耗時的向量操作,而且對每個資料項目的操作後的值是獨立的,如下面這個理想的例子所示:

type Vector []float64

// 應用操作到 v[i], v[i+1] ... v[n-1].
func (v Vector) DoSome(i, n int, u Vector, c chan int) {
  for ; i < n; i++ {
     v[i] += u.Op(v[i])
  }
  c <- 1 // 發送完成訊號
}

  我們在一個迴圈中為每個CPU啟動一個獨立的計算片段,這些片段可以以任意的順序執行,執行順序在這裡是無關緊要的。在啟動所有的goroutines後,我們只需要從通道中提取所有的完成訊號即可。

const NCPU = 4 // CPU核心數

func (v Vector) DoAll(u Vector) {
  c := make(chan int, NCPU) // Buffering optional but sensible.
  for i := 0; i < NCPU; i++ {
    go v.DoSome(i*len(v)/NCPU, (i+1)*len(v)/NCPU, u, c)
  }
  //從通道中取出所有訊號
  for i := 0; i < NCPU; i++ {
    <-c // 等待一個任務完成
  }
  // 至此全部任務均已完成.
}

  Go編譯器gc(6g等)的當前實現在預設情況下並不會使這段代碼並行化。對於使用者層級的進程,它僅使用單核。任意數目的goroutines都可以在系統調用中被阻滯,但是預設情形下任意時刻只能有一個goroutine可以執行使用者級代碼。如果你需要多核CPU的並行計算,必須通知運行時並存執行的goroutines數即GOMAXPROCS 。有兩種方式設定GOMAXPROCS,一個就是設定環境變數GOMAXPROCS,將其設為CPU核心數;另一種方式就是匯入runtime包並調用runtime.GOMAXPROCS(NPCU)。

(作者:瑪瑙河。尊重他人勞動成果,轉載請註明作者或出處)

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.