編譯自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)。
(作者:瑪瑙河。尊重他人勞動成果,轉載請註明作者或出處)