Go的研習筆記-day12(以Java的視角學習Go)

原文鏈接:https://github.com/unknwon/the-way-to-go_ZH_CN/blob/master/eBook/14.17.md

協程(goroutine)與通道(channel)
作爲一門 21 世紀的語言,Go 原生支持應用之間的通信(網絡,客戶端和服務端,分佈式計算)和程序的併發。程序可以在不同的處理器和計算機上同時執行不同的代碼段。Go 語言爲構建併發程序的基本代碼塊是 協程 (goroutine) 與通道 (channel)。他們需要語言,編譯器,和runtime的支持。Go 語言提供的垃圾回收器對併發編程至關重要。
不要通過共享內存來通信,而通過通信來共享內存。
通信強制協作。

  • 併發、並行和協程

  • 什麼是協程
    一個應用程序是運行在機器上的一個進程;進程是一個運行在自己內存地址空間裏的獨立執行體。一個進程由一個或多個操作系統線程組成,這些線程其實是共享同一個內存地址空間的一起工作的執行體。幾乎所有’正式’的程序都是多線程的,以便讓用戶或計算機不必等待,或者能夠同時服務多個請求(如 Web 服務器),或增加性能和吞吐量(例如,通過對不同的數據集並行執行代碼)。一個併發程序可以在一個處理器或者內核上使用多個線程來執行任務,但是隻有同一個程序在某個時間點同時運行在多核或者多處理器上纔是真正的並行。
    並行是一種通過使用多處理器以提高速度的能力。所以併發程序可以是並行的,也可以不是。
    公認的,使用多線程的應用難以做到準確,最主要的問題是內存中的數據共享,它們會被多線程以無法預知的方式進行操作,導致一些無法重現或者隨機的結果(稱作 競態)
    不要使用全局變量或者共享內存,它們會給你的代碼在併發運算的時候帶來危險。
    解決之道在於同步不同的線程,對數據加鎖,這樣同時就只有一個線程可以變更數據。在 Go 的標準庫 sync 中有一些工具用來在低級別的代碼中實現加鎖;過去的軟件開發經驗告訴我們這會帶來更高的複雜度,更容易使代碼出錯以及更低的性能,所以這個經典的方法明顯不再適合現代多核/多處理器編程:thread-per-connection 模型不夠有效。
    Go 更傾向於其他的方式,在諸多比較合適的範式中,有個被稱作 Communicating Sequential Processes(順序通信處理)(CSP, C. Hoare 發明的)還有一個叫做 message passing-model(消息傳遞)(已經運用在了其他語言中,比如 Erlang)。
    在 Go 中,應用程序併發處理的部分被稱作 goroutines(協程),它可以進行更有效的併發運算。在協程和操作系統線程之間並無一對一的關係:協程是根據一個或多個線程的可用性,映射(多路複用,執行於)在他們之上的;協程調度器在 Go 運行時很好的完成了這個工作。
    協程工作在相同的地址空間中,所以共享內存的方式一定是同步的;這個可以使用 sync 包來實現,不過我們很不鼓勵這樣做:Go 使用 channels 來同步協程
    當系統調用(比如等待 I/O)阻塞協程時,其他協程會繼續在其他線程上工作。協程的設計隱藏了許多線程創建和管理方面的複雜工作。
    協程是輕量的,比線程更輕。它們痕跡非常不明顯(使用少量的內存和資源):使用 4K 的棧內存就可以在堆中創建它們。因爲創建非常廉價,必要的時候可以輕鬆創建並運行大量的協程(在同一個地址空間中 100,000 個連續的協程)。並且它們對棧進行了分割,從而動態的增加(或縮減)內存的使用;棧的管理是自動的,但不是由垃圾回收器管理的,而是在協程退出後自動釋放。
    協程可以運行在多個操作系統線程之間,也可以運行在線程之內,讓你可以很小的內存佔用就可以處理大量的任務。由於操作系統線程上的協程時間片,你可以使用少量的操作系統線程就能擁有任意多個提供服務的協程,而且 Go 運行時可以聰明的意識到哪些協程被阻塞了,暫時擱置它們並處理其他協程。
    存在兩種併發方式:確定性的(明確定義排序)和非確定性的(加鎖/互斥從而未定義排序)。Go 的協程和通道理所當然的支持確定性的併發方式(例如通道具有一個 sender 和一個 receiver)。我們會在第 14.7 節中使用一個常見的算法問題(工人問題)來對比兩種處理方式。
    協程是通過使用關鍵字 go 調用(執行)一個函數或者方法來實現的(也可以是匿名或者 lambda 函數)。這樣會在當前的計算過程中開始一個同時進行的函數,在相同的地址空間中並且分配了獨立的棧,比如:go sum(bigArray),在後臺計算總和。
    協程的棧會根據需要進行伸縮,不出現棧溢出;開發者不需要關心棧的大小。當協程結束的時候,它會靜默退出:用來啓動這個協程的函數不會得到任何的返回值。
    任何 Go 程序都必須有的 main() 函數也可以看做是一個協程,儘管它並沒有通過 go 來啓動。協程可以在程序初始化的過程中運行(在 init() 函數中)。
    在一個協程中,比如它需要進行非常密集的運算,你可以在運算循環中週期的使用 runtime.Gosched():這會讓出處理器,允許運行其他協程;它並不會使當前協程掛起,所以它會自動恢復執行。使用 Gosched() 可以使計算均勻分佈,使通信不至於遲遲得不到響應

  • 併發和並行的差異
    Go 的併發原語提供了良好的併發設計基礎:表達程序結構以便表示獨立地執行的動作;所以Go的重點不在於並行的首要位置:併發程序可能是並行的,也可能不是。
    Go 默認沒有並行指令,只有一個獨立的核心或處理器被專門用於 Go 程序,不論它啓動了多少個協程;所以這些協程是併發運行的,但他們不是並行運行的:同一時間只有一個協程會處在運行狀態。
    這個情況在以後可能會發生改變,不過屆時,爲了使你的程序可以使用多個核心運行,這時協程就真正的是並行運行了,你必須使用 GOMAXPROCS 變量。
    這會告訴運行時有多少個協程同時執行。
    並且只有 gc 編譯器真正實現了協程,適當的把協程映射到操作系統線程。使用 gccgo 編譯器,會爲每一個協程創建操作系統線程。

  • 使用 GOMAXPROCS
    在 gc 編譯器下(6g 或者 8g)你必須設置 GOMAXPROCS 爲一個大於默認值 1 的數值來允許運行時支持使用多於 1 個的操作系統線程,所有的協程都會共享同一個線程除非將 GOMAXPROCS 設置爲一個大於 1 的數。當 GOMAXPROCS 大於 1 時,會有一個線程池管理許多的線程。通過 gccgo 編譯器 GOMAXPROCS 有效的與運行中的協程數量相等。假設 n 是機器上處理器或者核心的數量。如果你設置環境變量 GOMAXPROCS>=n,或者執行 runtime.GOMAXPROCS(n),接下來協程會被分割(分散)到 n 個處理器上。更多的處理器並不意味着性能的線性提升。有這樣一個經驗法則,對於 n 個核心的情況設置 GOMAXPROCS 爲 n-1 以獲得最佳性能,也同樣需要遵守這條規則:協程的數量 > 1 + GOMAXPROCS > 1。
    所以如果在某一時間只有一個協程在執行,不要設置 GOMAXPROCS!
    總結:GOMAXPROCS 等同於(併發的)線程數量,在一臺核心數多於1個的機器上,會儘可能有等同於核心數的線程在並行運行。

  • 如何用命令行指定使用的核心數量
    使用 flags 包,如下:
    var numCores = flag.Int(“n”, 2, “number of CPU cores to use”)
    在 main() 中:
    flag.Parse()
    runtime.GOMAXPROCS(*numCores)
    協程可以通過調用runtime.Goexit()來停止,儘管這樣做幾乎沒有必要。
    當 main() 函數返回的時候,程序退出:它不會等待任何其他非 main 協程的結束。這就是爲什麼在服務器程序中,每一個請求都會啓動一個協程來處理,server() 函數必須保持運行狀態。通常使用一個無限循環來達到這樣的目的。
    另外,協程是獨立的處理單元,一旦陸續啓動一些協程,你無法確定他們是什麼時候真正開始執行的。你的代碼邏輯必須獨立於協程調用的順序
    協程更有用的一個例子應該是在一個非常長的數組中查找一個元素。
    將數組分割爲若干個不重複的切片,然後給每一個切片啓動一個協程進行查找計算。這樣許多並行的協程可以用來進行查找任務,整體的查找時間會縮短(除以協程的數量)。

  • Go 協程(goroutines)和協程(coroutines)
    goroutines是Go 語言中的協程。而“協程(coroutines)”指的是其他語言中的協程概念
    在其他語言中,比如 C#,Lua 或者 Python 都有協程的概念。這個名字表明它和 Go協程有些相似,不過有兩點不同:
    Go 協程意味着並行(或者可以以並行的方式部署),協程一般來說不是這樣的
    Go 協程通過通道來通信;協程通過讓出和恢復操作來通信
    Go 協程比協程更強大,也很容易從協程的邏輯複用到 Go 協程。

  • 協程間的信道
  • 概念
    在go的例子中,協程是獨立執行的,他們之間沒有通信。他們必須通信纔會變得更有用:彼此之間發送和接收信息並且協調/同步他們的工作。協程可以使用共享變量來通信,但是很不提倡這樣做,因爲這種方式給所有的共享內存的多線程都帶來了困難。
    而 Go 有一種特殊的類型,通道(channel),就像一個可以用於發送類型化數據的管道,由其負責協程之間的通信,從而避開所有由共享內存導致的陷阱;這種通過通道進行通信的方式保證了同步性。數據在通道中進行傳遞:在任何給定時間,一個數據被設計爲只有一個協程可以對其訪問,所以不會發生數據競爭。 數據的所有權(可以讀寫數據的能力)也因此被傳遞。
    工廠的傳送帶是個很有用的例子。一個機器(生產者協程)在傳送帶上放置物品,另外一個機器(消費者協程)拿到物品並打包。
    通道服務於通信的兩個目的:值的交換,同步的,保證了兩個計算(協程)任何時候都是可知狀態。
    在這裏插入圖片描述
    通常使用這樣的格式來聲明通道:var identifier chan datatype
    未初始化的通道的值是nil。
    所以通道只能傳輸一種類型的數據,比如 chan int 或者 chan string,所有的類型都可以用於通道,空接口 interface{} 也可以。甚至可以(有時非常有用)創建通道的通道。
    通道實際上是類型化消息的隊列:使數據得以傳輸。它是先進先出(FIFO)的結構所以可以保證發送給他們的元素的順序(有些人知道,通道可以比作 Unix shells 中的雙向管道(two-way pipe))。通道也是引用類型,所以我們使用 make() 函數來給它分配內存。這裏先聲明瞭一個字符串通道 ch1,然後創建了它(實例化):
var ch1 chan string
ch1 = make(chan string)
當然可以更短: ch1 := make(chan string)。

這裏我們構建一個int通道的通道: chanOfChans := make(chan int)。

或者函數通道:funcChan := make(chan func())
所以通道是第一類對象:可以存儲在變量中,作爲函數的參數傳遞,從函數返回以及通過通道發送它們自身。另外它們是類型化的,允許類型檢查,比如嘗試使用整數通道發送一個指針。
  • 通信操作符 <-
    流向通道(發送)
    ch <- int1 表示:用通道 ch 發送變量 int1(雙目運算符,中綴 = 發送)
    從通道流出(接收),三種方式:
    int2 = <- ch 表示:變量 int2 從通道 ch(一元運算的前綴操作符,前綴 = 接收)接收數據(獲取新值);假設 int2 已經聲明過了,如果沒有的話可以寫成:int2 := <- ch
<- ch 可以單獨調用獲取通道的(下一個)值,當前值會被丟棄,但是可以用來驗證,所以以下代碼是合法的:

if <- ch != 1000{
	...
}
同一個操作符 <- 既用於發送也用於接收,但Go會根據操作對象弄明白該幹什麼 。
雖非強制要求,但爲了可讀性通道的命名通常以 ch 開頭或者包含 chan。
通道的發送和接收都是原子操作:它們總是互不干擾的完成的。
下面的示例展示了通信操作符的使用。
package main

import (
	"fmt"
	"time"
)

func main() {
	ch := make(chan string)

	go sendData(ch)
	go getData(ch)

	time.Sleep(1e9)
}

func sendData(ch chan string) {
	ch <- "Washington"
	ch <- "Tripoli"
	ch <- "London"
	ch <- "Beijing"
	ch <- "Tokyo"
}

func getData(ch chan string) {
	var input string
	// time.Sleep(2e9)
	for {
		input = <-ch
		fmt.Printf("%s ", input)
	}
}
  • 通道阻塞
    默認情況下,通信是同步且無緩衝的:在有接受者接收數據之前,發送不會結束。可以想象一個無緩衝的通道在沒有空間來保存數據的時候:必須要一個接收者準備好接收通道的數據然後發送者可以直接把數據發送給接收者。所以通道的發送/接收操作在對方準備好之前是阻塞的:
    1)對於同一個通道,發送操作(協程或者函數中的),在接收者準備好之前是阻塞的:如果ch中的數據無人接收,就無法再給通道傳入其他數據:新的輸入無法在通道非空的情況下傳入。所以發送操作會等待 ch 再次變爲可用狀態:就是通道值被接收時(可以傳入變量)。
    2)對於同一個通道,接收操作是阻塞的(協程或函數中的),直到發送者可用:如果通道中沒有數據,接收者就阻塞了。
    儘管這看上去是非常嚴格的約束,實際在大部分情況下工作的很不錯。
    程序 channel_block.go 驗證了以上理論,一個協程在無限循環中給通道發送整數數據。不過因爲沒有接收者,只輸出了一個數字 0。
package main

import "fmt"

func main() {
	ch1 := make(chan int)
	go pump(ch1)       // pump hangs
	fmt.Println(<-ch1) // prints only 0
}

func pump(ch chan int) {
	for i := 0; ; i++ {
		ch <- i
	}
}
pump() 函數爲通道提供數值,也被叫做生產者。

爲通道解除阻塞定義了 suck 函數來在無限循環中讀取通道
func suck(ch chan int) {
	for {
		fmt.Println(<-ch)
	}
}
在 main() 中使用協程開始它:

go pump(ch1)
go suck(ch1)
time.Sleep(1e9)
給程序 1 秒的時間來運行:輸出了上萬個整數。
  • 通過一個(或多個)通道交換數據進行協程同步。
    通信是一種同步形式:通過通道,兩個協程在通信(協程會和)中某刻同步交換數據。無緩衝通道成爲了多個協程同步的完美工具。
    甚至可以在通道兩端互相阻塞對方,形成了叫做死鎖的狀態。Go 運行時會檢查並 panic,停止程序。死鎖幾乎完全是由糟糕的設計導致的。
    無緩衝通道會被阻塞。設計無阻塞的程序可以避免這種情況,或者使用帶緩衝的通道。
解釋爲什麼下邊這個程序會導致 panic:所有的協程都休眠了 - 死鎖!

package main

import (
	"fmt"
)

func f1(in chan int) {
	fmt.Println(<-in)
}

func main() {
	out := make(chan int)
	out <- 2
	go f1(out)
}
  • 同步通道-使用帶緩衝的通道
    一個無緩衝通道只能包含 1 個元素,有時顯得很侷限。我們給通道提供了一個緩存,可以在擴展的 make 命令中設置它的容量,如下:
buf := 100
ch1 := make(chan string, buf)

buf 是通道可以同時容納的元素(這裏是 string)個數
在緩衝滿載(緩衝被全部使用)之前,給一個帶緩衝的通道發送數據是不會阻塞的,而從通道讀取數據也不會阻塞,直到緩衝空了。
緩衝容量和類型無關,所以可以(儘管可能導致危險)給一些通道設置不同的容量,只要他們擁有同樣的元素類型。內置的 cap 函數可以返回緩衝區的容量。
如果容量大於 0,通道就是異步的了:緩衝滿載(發送)或變空(接收)之前通信不會阻塞,元素會按照發送的順序被接收。如果容量是0或者未設置,通信僅在收發雙方準備好的情況下纔可以成功
同步:ch :=make(chan type, value)
value == 0 -> synchronous, unbuffered (阻塞)
value > 0 -> asynchronous, buffered(非阻塞)取決於value元素
若使用通道的緩衝,你的程序會在“請求”激增的時候表現更好:更具彈性,專業術語叫:更具有伸縮性(scalable)。要在首要位置使用無緩衝通道來設計算法,只在不確定的情況下使用緩衝。

  • 協程中用通道輸出結果
    爲了知道計算何時完成,可以通過信道回報。在例子 go sum(bigArray) 中,要這樣寫:
ch := make(chan int)
go sum(bigArray, ch) // bigArray puts the calculated sum on ch
// .. do something else for a while
sum := <- ch // wait for, and retrieve the sum

也可以使用通道來達到同步的目的,這個很有效的用法在傳統計算機中稱爲信號量(semaphore)。或者換個方式:通過通道發送信號告知處理已經完成(在協程中)。
在其他協程運行時讓 main 程序無限阻塞的通常做法是在 main 函數的最後放置一個 select {}。
也可以使用通道讓 main 程序等待協程完成,就是所謂的信號量模式

  • 信號量模式
    下邊的片段闡明:協程通過在通道 ch 中放置一個值來處理結束的信號。main 協程等待 <-ch 直到從中獲取到值。
    我們期望從這個通道中獲取返回的結果,像這樣
func compute(ch chan int){
	ch <- someComputation() // when it completes, signal on the channel.
}

func main(){
	ch := make(chan int) 	// allocate a channel.
	go compute(ch)		// stat something in a goroutines
	doSomethingElseForAWhile()
	result := <- ch
}

這個信號也可以是其他的,不返回結果,比如下面這個協程中的匿名函數(lambda)協程:

ch := make(chan int)
go func(){
	// doSomething
	ch <- 1 // Send a signal; value does not matter
}()
doSomethingElseForAWhile()
<- ch	// Wait for goroutine to finish; discard sent value.

或者等待兩個協程完成,每一個都會對切片s的一部分進行排序,片段如下:

done := make(chan bool)
// doSort is a lambda function, so a closure which knows the channel done:
doSort := func(s []int){
	sort(s)
	done <- true
}
i := pivot(s)
go doSort(s[:i])
go doSort(s[i:])
<-done
<-done

下邊的代碼,用完整的信號量模式對長度爲N的 float64 切片進行了 N 個 doSomething() 計算並同時完成,通道 sem 分配了相同的長度(且包含空接口類型的元素),待所有的計算都完成後,發送信號(通過放入值)。在循環中從通道 sem 不停的接收數據來等待所有的協程完成。

type Empty interface {}
var empty Empty
...
data := make([]float64, N)
res := make([]float64, N)
sem := make(chan Empty, N)
...
for i, xi := range data {
	go func (i int, xi float64) {
		res[i] = doSomething(i, xi)
		sem <- empty
	} (i, xi)
}
// wait for goroutines to finish
for i := 0; i < N; i++ { <-sem }

注意上述代碼中閉合函數的用法:i、xi 都是作爲參數傳入閉合函數的,這一做法使得每個協程(譯者注:在其啓動時)獲得一份 i 和 xi 的單獨拷貝,從而向閉合函數內部屏蔽了外層循環中的 i 和 xi變量;否則,for 循環的下一次迭代會更新所有協程中 i 和 xi 的值。另一方面,切片 res 沒有傳入閉合函數,因爲協程不需要res的單獨拷貝。切片 res 也在閉合函數中但並不是參數。

  • 實現並行的 for 循環
    for 循環的每一個迭代是並行完成的:
for i, v := range data {
	go func (i int, v float64) {
		doSomething(i, v)
		...
	} (i, v)
}
  • 用帶緩衝通道實現一個信號量
    信號量是實現互斥鎖(排外鎖)常見的同步機制,限制對資源的訪問,解決讀寫問題,比如沒有實現信號量的 sync 的 Go 包,使用帶緩衝的通道可以輕鬆實現:
  • 帶緩衝通道的容量和要同步的資源容量相同
  • 通道的長度(當前存放的元素個數)與當前資源被使用的數量相同
  • 容量減去通道的長度就是未處理的資源個數(標準信號量的整數值)
    不用管通道中存放的是什麼,只關注長度;因此我們創建了一個長度可變但容量爲0(字節)的通道:
    type Empty interface {}
    type semaphore chan Empty
    將可用資源的數量N來初始化信號量 semaphore:sem = make(semaphore, N)
然後直接對信號量進行操作:

// acquire n resources
func (s semaphore) P(n int) {
	e := new(Empty)
	for i := 0; i < n; i++ {
		s <- e
	}
}

// release n resouces
func (s semaphore) V(n int) {
	for i:= 0; i < n; i++{
		<- s
	}
}
可以用來實現一個互斥的例子:

/* mutexes */
func (s semaphore) Lock() {
	s.P(1)
}

func (s semaphore) Unlock(){
	s.V(1)
}

/* signal-wait */
func (s semaphore) Wait(n int) {
	s.P(n)
}

func (s semaphore) Signal() {
	s.V(1)
}

習慣用法:通道工廠模式
編程中常見的另外一種模式如下:不將通道作爲參數傳遞給協程,而用函數來生成一個通道並返回(工廠角色);函數內有個匿名函數被協程調用。

package main

import (
	"fmt"
	"time"
)

func main() {
	stream := pump()
	go suck(stream)
	time.Sleep(1e9)
}

func pump() chan int {
	ch := make(chan int)
	go func() {
		for i := 0; ; i++ {
			ch <- i
		}
	}()
	return ch
}

func suck(ch chan int) {
	for {
		fmt.Println(<-ch)
	}
}
  • 給通道使用 for 循環
    for 循環的 range 語句可以用在通道 ch 上,便可以從通道中獲取值
for v := range ch {
	fmt.Printf("The value is %v\n", v)
}
它從指定通道中讀取數據直到通道關閉,才繼續執行下邊的代碼。
很明顯,另外一個協程必須寫入 ch(不然代碼就阻塞在 for 循環了),而且必須在寫入完成後才關閉。
suck 函數可以這樣寫,且在協程中調用這個動作,程序變成了這樣:
package main

import (
	"fmt"
	"time"
)

func main() {
	suck(pump())
	time.Sleep(1e9)
}

func pump() chan int {
	ch := make(chan int)
	go func() {
		for i := 0; ; i++ {
			ch <- i
		}
	}()
	return ch
}

func suck(ch chan int) {
	go func() {
		for v := range ch {
			fmt.Println(v)
		}
	}()
}
  • 習慣用法:通道迭代模式
    這個模式用到了生產者-消費者模式,通常,需要從包含了地址索引字段 items 的容器給通道填入元素。爲容器的類型定義一個方法 Iter(),返回一個只讀的通道items,如下:
func (c *container) Iter () <- chan item {
	ch := make(chan item)
	go func () {
		for i:= 0; i < c.Len(); i++{	// or use a for-range loop
			ch <- c.items[i]
		}
	} ()
	return ch
}

在協程裏,一個 for 循環迭代容器 c 中的元素(對於樹或圖的算法,這種簡單的 for 循環可以替換爲深度優先搜索)。
調用這個方法的代碼可以這樣迭代容器:
for x := range container.Iter() { … }
其運行在自己啓動的協程中,所以上邊的迭代用到了一個通道和兩個協程(可能運行在不同的線程上)。 這樣我們就有了一個典型的生產者-消費者模式。如果程序在協程給通道寫完值之前結束,則這個協程不會被垃圾回收;這是設計使然。這種看起來並不符合預期的行爲正是由通道這種線程安全的通信方式所導致的。如此一來,一個協程爲了寫入一個永遠無人讀取的通道而被掛起就成了一個bug,而並非你預想中的那樣被悄悄回收掉(garbage-collected)了。
習慣用法:生產者消費者模式
假設你有 Produce() 函數來產生 Consume 函數需要的值。它們都可以運行在獨立的協程中,生產者在通道中放入給消費者讀取的值。整個處理過程可以替換爲無限循環:
for {
Consume(Produce())
}

  • 通道的方向
    通道類型可以用註解來表示它只發送或者只接收:
    var send_only chan<- int // channel can only receive data
    var recv_only <-chan int // channel can only send data
    只接收的通道(chan<- T)無法關閉,因爲關閉通道是發送者用來表示不再給通道發送值了,所以對只接收通道是沒有意義的。通道創建的時候都是雙向的,但也可以分配有方向的通道變量,就像以下代碼:
var c = make(chan int) // bidirectional
go source(c)
go sink(c)

func source(ch chan<- int){
	for { ch <- 1 }
}

func sink(ch <-chan int) {
	for { <-ch }
}
  • 習慣用法:管道和選擇器模式
sendChan := make(chan int)
receiveChan := make(chan string)
go processChannel(sendChan, receiveChan)

func processChannel(in <-chan int, out chan<- string) {
	for inValue := range in {
		result := ... /// processing inValue
		out <- result
	}
}

通過使用方向註解來限制協程對通道的操作。
這裏有一個來自 Go 指導的很讚的例子,打印了輸出的素數,使用選擇器(‘篩’)作爲它的算法。每個 prime 都有一個選擇器,如下圖:在這裏插入圖片描述

// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.package main
package main

import "fmt"

// Send the sequence 2, 3, 4, ... to channel 'ch'.
func generate(ch chan int) {
	for i := 2; ; i++ {
		ch <- i // Send 'i' to channel 'ch'.
	}
}

// Copy the values from channel 'in' to channel 'out',
// removing those divisible by 'prime'.
func filter(in, out chan int, prime int) {
	for {
		i := <-in // Receive value of new variable 'i' from 'in'.
		if i%prime != 0 {
			out <- i // Send 'i' to channel 'out'.
		}
	}
}

// The prime sieve: Daisy-chain filter processes together.
func main() {
	ch := make(chan int) // Create a new channel.
	go generate(ch)      // Start generate() as a goroutine.
	for {
		prime := <-ch
		fmt.Print(prime, " ")
		ch1 := make(chan int)
		go filter(ch, ch1, prime)
		ch = ch1
	}
}
協程 filter(in, out chan int, prime int) 拷貝整數到輸出通道,丟棄掉可以被 prime 整除的數字。然後每個 prime 又開啓了一個新的協程,生成器和選擇器併發請求。
  • 協程的同步:關閉通道-測試阻塞的通道
    通道可以被顯式的關閉;儘管它們和文件不同:不必每次都關閉。只有在當需要告訴接收者不會再提供新的值的時候,才需要關閉通道。只有發送者需要關閉通道,接收者永遠不會需要。
    第一個可以通過函數 close(ch) 來完成:這個將通道標記爲無法通過發送操作 <- 接受更多的值;給已經關閉的通道發送或者再次關閉都會導致運行時的 panic。在創建一個通道後使用 defer 語句是個不錯的辦法(類似這種情況):
ch := make(chan float64)
defer close(ch)
第二個問題可以使用逗號,ok 操作符:用來檢測通道是否被關閉。
如何來檢測可以收到沒有被阻塞(或者通道沒有被關閉)?
v, ok := <-ch   // ok is true if v received value
通常和 if 語句一起使用:

if v, ok := <-ch; ok {
  process(v)
}
或者在 for 循環中接收的時候,當關閉或者阻塞的時候使用 break:

v, ok := <-ch
if !ok {
  break
}
process(v)

實現非阻塞通道的讀取,需要使用 select
package main

import "fmt"

func main() {
	ch := make(chan string)
	go sendData(ch)
	getData(ch)
}

func sendData(ch chan string) {
	ch <- "Washington"
	ch <- "Tripoli"
	ch <- "London"
	ch <- "Beijing"
	ch <- "Tokio"
	close(ch)
}

func getData(ch chan string) {
	for {
		input, open := <-ch
		if !open {
			break
		}
		fmt.Printf("%s ", input)
	}
}

通道迭代器中,兩個協程經常是一個阻塞另外一個。如果程序工作在多核心的機器上,大部分時間只用到了一個處理器。可以通過使用帶緩衝(緩衝空間大於 0)的通道來改善。比如,緩衝大小爲 100,迭代器在阻塞之前,至少可以從容器獲得 100 個元素。如果消費者協程在獨立的內核運行,就有可能讓協程不會出現阻塞。
由於容器中元素的數量通常是已知的,需要讓通道有足夠的容量放置所有的元素。這樣,迭代器就不會阻塞(儘管消費者協程仍然可能阻塞)。然而,這實際上加倍了迭代容器所需要的內存使用量,所以通道的容量需要限制一下最大值。記錄運行時間和性能測試可以幫助你找到最小的緩存容量帶來最好的性能。

  • 使用 select 切換協程
    從不同的併發執行的協程中獲取值可以通過關鍵字select來完成,它和switch控制語句非常相似也被稱作通信開關;它的行爲像是“你準備好了嗎”的輪詢機制;select監聽進入通道的數據,也可以是用通道發送值的時候。
select {
case u:= <- ch1:
        ...
case v:= <- ch2:
        ...
        ...
default: // no value ready to be received
        ...
}
default 語句是可選的;fallthrough 行爲,和普通的 switch 相似,是不允許的。在任何一個 case 中執行 break 或者 return,select 就結束了。
select 做的就是:選擇處理列出的多個通信情況中的一個。
如果都阻塞了,會等待直到其中一個可以處理
如果多個可以處理,隨機選擇一個
如果沒有通道操作可以處理並且寫了 default 語句,它就會執行:default 永遠是可運行的(這就是準備好了,可以執行)
在 select 中使用發送操作並且有 default 可以確保發送不被阻塞!如果沒有 default,select 就會一直阻塞。

select 語句實現了一種監聽模式,通常用在(無限)循環中;在某種情況下,通過 break 語句使循環退出。

在程序 goroutine_select.go 中有 2 個通道 ch1 和 ch2,三個協程 pump1()、pump2() 和 suck()。這是一個典型的生產者消費者模式。在無限循環中,ch1 和 ch2 通過 pump1() 和 pump2() 填充整數;suck() 也是在無限循環中輪詢輸入的,通過 select 語句獲取 ch1 和 ch2 的整數並輸出。選擇哪一個 case 取決於哪一個通道收到了信息。程序在 main 執行 1 秒後結束。
-goroutine_select.go:

package main

import (
	"fmt"
	"time"
)

func main() {
	ch1 := make(chan int)
	ch2 := make(chan int)

	go pump1(ch1)
	go pump2(ch2)
	go suck(ch1, ch2)

	time.Sleep(1e9)
}

func pump1(ch chan int) {
	for i := 0; ; i++ {
		ch <- i * 2
	}
}

func pump2(ch chan int) {
	for i := 0; ; i++ {
		ch <- i + 5
	}
}

func suck(ch1, ch2 chan int) {
	for {
		select {
		case v := <-ch1:
			fmt.Printf("Received on channel 1: %d\n", v)
		case v := <-ch2:
			fmt.Printf("Received on channel 2: %d\n", v)
		}
	}
}

習慣用法:後臺服務模式
服務通常是是用後臺協程中的無限循環實現的,在循環中使用 select 獲取並處理通道中的數據:

// Backend goroutine.
func backend() {
	for {
		select {
		case cmd := <-ch1:
			// Handle ...
		case cmd := <-ch2:
			...
		case cmd := <-chStop:
			// stop server
		}
	}
}
在程序的其他地方給通道 ch1,ch2 發送數據,比如:通道 stop 用來清理結束服務程序。
另一種方式(但是不太靈活)就是(客戶端)在 chRequest 上提交請求,後臺協程循環這個通道,使用 switch 根據請求的行爲來分別處理:
func backend() {
	for req := range chRequest {
		switch req.Subjext() {
			case A1:  // Handle case ...
			case A2:  // Handle case ...
			default:
			  // Handle illegal request ..
			  // ...
		}
	}
}
  • 通道、超時和計時器(Ticker)
    time 包中有一些有趣的功能可以和通道組合使用。
    其中就包含了 time.Ticker 結構體,這個對象以指定的時間間隔重複的向通道 C 發送時間值:
type Ticker struct {
    C <-chan Time // the channel on which the ticks are delivered.
    // contains filtered or unexported fields
    ...
}
時間間隔的單位是 ns(納秒,int64),在工廠函數 time.NewTicker 中以 Duration 類型的參數傳入:func NewTicker(dur) *Ticker。

在協程週期性的執行一些事情(打印狀態日誌,輸出,計算等等)的時候非常有用。

調用 Stop() 使計時器停止,在 defer 語句中使用。這些都很好的適應 select 語句:
ticker := time.NewTicker(updateInterval)
defer ticker.Stop()
...
select {
case u:= <-ch1:
    ...
case v:= <-ch2:
    ...
case <-ticker.C:
    logState(status) // call some logging function logState
default: // no value ready to be received
    ...
}

time.Tick() 函數聲明爲 Tick(d Duration) <-chan Time,當你想返回一個通道而不必關閉它的時候這個函數非常有用:它以 d 爲週期給返回的通道發送時間,d是納秒數。如果需要像下邊的代碼一樣,限制處理頻率(函數 client.Call() 是一個 RPC 調用,這裏暫不贅述

import "time"

rate_per_sec := 10
var dur Duration = 1e9 / rate_per_sec
chRate := time.Tick(dur) // a tick every 1/10th of a second
for req := range requests {
    <- chRate // rate limit our Service.Method RPC calls
    go client.Call("Service.Method", req, ...)
}
這樣只會按照指定頻率處理請求:chRate 阻塞了更高的頻率。每秒處理的頻率可以根據機器負載(和/或)資源的情況而增加或減少。
  • 簡單超時模式
    要從通道 ch 中接收數據,但是最多等待1秒。先創建一個信號通道,然後啓動一個 lambda 協程,協程在給通道發送數據之前是休眠的:
timeout := make(chan bool, 1)
go func() {
        time.Sleep(1e9) // one second
        timeout <- true
}()
然後使用 select 語句接收 ch 或者 timeout 的數據:如果 ch 在 1 秒內沒有收到數據,就選擇到了 time 分支並放棄了 ch 的讀取。

select {
    case <-ch:
        // a read from ch has occured
    case <-timeout:
        // the read from ch has timed out
        break
}
  • 取消耗時很長的同步調用
    也可以使用 time.After() 函數替換 timeout-channel。可以在 select 中通過 time.After() 發送的超時信號來停止協程的執行。以下代碼,在 timeoutNs 納秒後執行 select 的 timeout 分支後,執行client.Call 的協程也隨之結束,不會給通道 ch 返回值:
ch := make(chan error, 1)
go func() { ch <- client.Call("Service.Method", args, &reply) } ()
select {
case resp := <-ch
    // use resp and reply
case <-time.After(timeoutNs):
    // call timed out
    break
}

注意緩衝大小設置爲 1 是必要的,可以避免協程死鎖以及確保超時的通道可以被垃圾回收。此外,需要注意在有多個 case 符合條件時, select 對 case 的選擇是僞隨機的,如果上面的代碼稍作修改如下,則 select 語句可能不會在定時器超時信號到來時立刻選中 time.After(timeoutNs) 對應的 case,因此協程可能不會嚴格按照定時器設置的時間結束。

ch := make(chan int, 1)
go func() { for { ch <- 1 } } ()
L:
for {
    select {
    case <-ch:
        // do something
    case <-time.After(timeoutNs):
        // call timed out
        break L
    }
}
  • 假設程序從多個複製的數據庫同時讀取。只需要一個答案,需要接收首先到達的答案,Query 函數獲取數據庫的連接切片並請求。並行請求每一個數據庫並返回收到的第一個響應:
func Query(conns []Conn, query string) Result {
    ch := make(chan Result, 1)
    for _, conn := range conns {
        go func(c Conn) {
            select {
            case ch <- c.DoQuery(query):
            default:
            }
        }(conn)
    }
    return <- ch
}

再次聲明,結果通道 ch 必須是帶緩衝的:以保證第一個發送進來的數據有地方可以存放,確保放入的首個數據總會成功,所以第一個到達的值會被獲取而與執行的順序無關。正在執行的協程可以總是可以使用 runtime.Goexit() 來停止。
在應用中緩存數據:
應用程序中用到了來自數據庫(或者常見的數據存儲)的數據時,經常會把數據緩存到內存中,因爲從數據庫中獲取數據的操作代價很高;如果數據庫中的值不發生變化就沒有問題。但是如果值有變化,我們需要一個機制來週期性的從數據庫重新讀取這些值:緩存的值就不可用(過期)了,而且我們也不希望用戶看到陳舊的數據

  • 協程和恢復(recover)
    一個用到 recover 的程序停掉了服務器內部一個失敗的協程而不影響其他協程的工作。
func server(workChan <-chan *Work) {
    for work := range workChan {
        go safelyDo(work)   // start the goroutine for that work
    }
}

func safelyDo(work *Work) {
    defer func() {
        if err := recover(); err != nil {
            log.Printf("Work failed with %s in %v", err, work)
        }
    }()
    do(work)
}

上邊的代碼,如果 do(work) 發生 panic,錯誤會被記錄且協程會退出並釋放,而其他協程不受影響。
因爲 recover 總是返回 nil,除非直接在 defer 修飾的函數中調用,defer 修飾的代碼可以調用那些自身可以使用 panic 和 recover 避免失敗的庫例程(庫函數)。舉例,safelyDo() 中 defer 修飾的函數可能在調用 recover 之前就調用了一個 logging 函數,panicking 狀態不會影響 logging 代碼的運行。因爲加入了恢復模式,函數 do(以及它調用的任何東西)可以通過調用 panic 來擺脫不好的情況。但是恢復是在 panicking 的協程內部的:不能被另外一個協程恢復。

  • 新舊模型對比:任務和worker
    假設我們需要處理很多任務;一個worker處理一項任務。任務可以被定義爲一個結構體(具體的細節在這裏並不重要):
type Task struct {
    // some state
}
  • 舊模式:使用共享內存進行同步
    由各個任務組成的任務池共享內存;爲了同步各個worker以及避免資源競爭,我們需要對任務池進行加鎖保護
    type Pool struct {
    Mu sync.Mutex
    Tasks []*Task
    }
    sync.Mutex(互斥鎖:它用來在代碼中保護臨界區資源:同一時間只有一個go協程(goroutine)可以進入該臨界區。如果出現了同一時間多個go協程都進入了該臨界區,則會產生競爭:Pool結構就不能保證被正確更新。在傳統的模式中(經典的面向對象的語言中應用得比較多,比如C++,JAVA,C#),worker代碼可能這樣寫:

func Worker(pool *Pool) {
    for {
        pool.Mu.Lock()
        // begin critical section:
        task := pool.Tasks[0]        // take the first task
        pool.Tasks = pool.Tasks[1:]  // update the pool of tasks
        // end critical section
        pool.Mu.Unlock()
        process(task)
    }
}

這些worker有許多都可以併發執行;他們可以在go協程中啓動。一個worker先將pool鎖定,從pool獲取第一項任務,再解鎖和處理任務。加鎖保證了同一時間只有一個go協程可以進入到pool中:一項任務有且只能被賦予一個worker。如果不加鎖,則工作協程可能會在task:=pool.Tasks[0]發生切換,導致pool.Tasks=pool.Tasks[1:]結果異常:一些worker獲取不到任務,而一些任務可能被多個worker得到。加鎖實現同步的方式在工作協程比較少時可以工作的很好,但是當工作協程數量很大,任務量也很多時,處理效率將會因爲頻繁的加鎖/解鎖開銷而降低。當工作協程數增加到一個閾值時,程序效率會急劇下降,這就成爲了瓶頸。

  • 新模式:使用通道
    使用通道進行同步:使用一個通道接受需要處理的任務,一個通道接受處理完成的任務(及其結果)。worker在協程中啓動,其數量N應該根據任務數量進行調整。
    主線程扮演着Master節點角色,可能寫成如下形式:
func main() {
        pending, done := make(chan *Task), make(chan *Task)
        go sendWork(pending)       // put tasks with work on the channel
        for i := 0; i < N; i++ {   // start N goroutines to do work
            go Worker(pending, done)
        }
        consumeWork(done)          // continue with the processed tasks
    }
    worker的邏輯比較簡單:從pending通道拿任務,處理後將其放到done通道中:
    func Worker(in, out chan *Task) {
        for {
            t := <-in
            process(t)
            out <- t
        }
    }
    這裏並不使用鎖:從通道得到新任務的過程沒有任何競爭。隨着任務數量增加,worker數量也應該相應增加,同時性能並不會像第一種方式那樣下降明顯。
    在pending通道中存在一份任務的拷貝,第一個worker從pending通道中獲得第一個任務並進行處理,這裏並不存在競爭(對一個通道讀數據和寫數據的整個過程是原子性的)。
    某一個任務會在哪一個worker中被執行是不可知的,反過來也是。worker數量的增多也會增加通信的開銷,這會對性能有輕微的影響。
從這個簡單的例子中可能很難看出第二種模式的優勢,但含有複雜鎖運用的程序不僅在編寫上顯得困難,也不容易編寫正確,使用第二種模式的話,就無需考慮這麼複雜的東西了。
因此,第二種模式對比第一種模式而言,不僅性能是一個主要優勢,而且還有個更大的優勢:代碼顯得更清晰、更優雅。一個更符合go語言習慣的worker寫法
Use an in- and out-channel instead of locking
func Worker(in, out chan *Task) {
        for {
            t := <-in
            process(t)
            out <- t
        }
    }
    對於任何可以建模爲Master-Worker範例的問題,一個類似於worker使用通道進行通信和交互、Master進行整體協調的方案都能完美解決。
    如果系統部署在多臺機器上,各個機器上執行Worker協程,Master和Worker之間使用netchan或者RPC進行通信
  • 怎麼選擇是該使用鎖還是通道?
    通道是一個較新的概念,本節我們着重強調了在go協程裏通道的使用,但這並不意味着經典的鎖方法就不能使用。go語言讓你可以根據實際問題進行選擇:創建一個優雅、簡單、可讀性強、在大多數場景性能表現都能很好的方案。如果你的問題適合使用鎖,也不要忌諱使用它。go語言注重實用,什麼方式最能解決你的問題就用什麼方式,而不是強迫你使用一種編碼風格。下面列出一個普遍的經驗法則:
  • 使用鎖的情景:
  • 訪問共享數據結構中的緩存信息
  • 保存應用程序上下文和狀態信息數據
  • 使用通道的情景:
  • 與異步操作的結果進行交互
  • 分發任務
  • 傳遞數據所有權
  • 惰性生成器的實現
    生成器是指當被調用時返回一個序列中下一個值的函數,例如:
  generateInteger() => 0
    generateInteger() => 1
    generateInteger() => 2
    ....

生成器每次返回的是序列中下一個值而非整個序列;這種特性也稱之爲惰性求值:只在你需要時進行求值,同時保留相關變量資源(內存和cpu):這是一項在需要時對表達式進行求值的技術。例如,生成一個無限數量的偶數序列:要產生這樣一個序列並且在一個一個的使用可能會很困難,而且內存會溢出!但是一個含有通道和go協程的函數能輕易實現這個需求。

實現了一個使用 int 型通道來實現的生成器。通道被命名爲yield和resume,這些詞經常在協程代碼中使用。
package main

import (
    "fmt"
)

var resume chan int

func integers() chan int {
    yield := make(chan int)
    count := 0
    go func() {
        for {
            yield <- count
            count++
        }
    }()
    return yield
}

func generateInteger() int {
    return <-resume
}

func main() {
    resume = integers()
    fmt.Println(generateInteger())  //=> 0
    fmt.Println(generateInteger())  //=> 1
    fmt.Println(generateInteger())  //=> 2    
}

有一個細微的區別是從通道讀取的值可能會是稍早前產生的,並不是在程序被調用時生成的。如果確實需要這樣的行爲,就得實現一個請求響應機制。當生成器生成數據的過程是計算密集型且各個結果的順序並不重要時,那麼就可以將生成器放入到go協程實現並行化。但是得小心,使用大量的go協程的開銷可能會超過帶來的性能增益。
這些原則可以概括爲:通過巧妙地使用空接口、閉包和高階函數,我們能實現一個通用的惰性生產器的工廠函數BuildLazyEvaluator(這個應該放在一個工具包中實現)。工廠函數需要一個函數和一個初始狀態作爲輸入參數,返回一個無參、返回值是生成序列的函數。傳入的函數需要計算出下一個返回值以及下一個狀態參數。在工廠函數中,創建一個通道和無限循環的go協程。返回值被放到了該通道中,返回函數稍後被調用時從該通道中取得該返回值。每當取得一個值時,下一個值即被計算。在下面的例子中,定義了一個evenFunc函數,其是一個惰性生成函數:在main函數中,我們創建了前10個偶數,每個都是通過調用even()函數取得下一個值的。爲此,我們需要在BuildLazyIntEvaluator函數中具體化我們的生成函數,然後我們能夠基於此做出定義。

package main

import (
    "fmt"
)

type Any interface{}
type EvalFunc func(Any) (Any, Any)

func main() {
    evenFunc := func(state Any) (Any, Any) {
        os := state.(int)
        ns := os + 2
        return os, ns
    }
    
    even := BuildLazyIntEvaluator(evenFunc, 0)
    
    for i := 0; i < 10; i++ {
        fmt.Printf("%vth even: %v\n", i, even())
    }
}

func BuildLazyEvaluator(evalFunc EvalFunc, initState Any) func() Any {
    retValChan := make(chan Any)
    loopFunc := func() {
        var actState Any = initState
        var retVal Any
        for {
            retVal, actState = evalFunc(actState)
            retValChan <- retVal
        }
    }
    retFunc := func() Any {
        return <- retValChan
    }
    go loopFunc()
    return retFunc
}

func BuildLazyIntEvaluator(evalFunc EvalFunc, initState Any) func() int {
    ef := BuildLazyEvaluator(evalFunc, initState)
    return func() int {
        return ef().(int)
    }
}
  • 實現 Futures 模式
    所謂Futures就是指:有時候在你使用某一個值之前需要先對其進行計算。這種情況下,你就可以在另一個處理器上進行該值的計算,到使用時,該值就已經計算完畢了
    http://www.golangpatterns.info/concurrency/futures
  • 複用
  • 典型的客戶端/服務器(C/S)模式
    客戶端-服務器應用正是 goroutines 和 channels 的亮點所在。
    客戶端(Client)可以是運行在任意設備上的任意程序,它會按需發送請求(request)至服務器。服務器(Server)接收到這個請求後開始相應的工作,然後再將響應(response)返回給客戶端。典型情況下一般是多個客戶端(即多個請求)對應一個(或少量)服務器。例如我們日常使用的瀏覽器客戶端,其功能就是向服務器請求網頁。而Web服務器則會向瀏覽器響應網頁數據。
    使用Go的服務器通常會在協程中執行向客戶端的響應,故而會對每一個客戶端請求啓動一個協程。一個常用的操作方法是客戶端請求自身中包含一個通道,而服務器則向這個通道發送響應
  • 卸載(Teardown):通過信號通道關閉服務器
  • 限制同時處理的請求數
    使用帶緩衝區的通道很容易實現這一點,其緩衝區容量就是同時處理請求的最大數量。程序max_tasks.go雖然沒有做什麼有用的事但是卻包含了這個技巧:超過MAXREQS的請求將不會被同時處理,因爲當信號通道表示緩衝區已滿時handle函數會阻塞且不再處理其他請求,直到某個請求從sem中被移除。sem就像一個信號量,這一專業術語用於在程序中表示特定條件的標誌變量。
package main

const MAXREQS = 50

var sem = make(chan int, MAXREQS)

type Request struct {
	a, b   int
	replyc chan int
}

func process(r *Request) {
	// do something
}

func handle(r *Request) {
	sem <- 1 // doesn't matter what we put in it
	process(r)
	<-sem // one empty place in the buffer: the next request can start
}

func server(service chan *Request) {
	for {
		request := <-service
		go handle(request)
	}
}

func main() {
	service := make(chan *Request)
	go server(service)
}

通過這種方式,應用程序可以通過使用緩衝通道(通道被用作信號量)使協程同步其對該資源的使用,從而充分利用有限的資源(如內存)

  • 鏈式協程
    下面的演示程序 chaining.go 再次展示了啓動巨量的Go協程是多麼容易。這些協程已全部在 main 函數中的 for 循環裏啓動。當循環完成之後,一個0被寫入到最右邊的通道里,於是100,000個協程開始執行,接着1000000這個結果會在1.5秒之內被打印出來。
    這個程序同時也展示瞭如何通過flag.Int來解析命令行中的參數以指定協程數量,例如:chaining -n=7000會生成7000個協程
package main

import (
	"flag"
	"fmt"
)

var ngoroutine = flag.Int("n", 100000, "how many goroutines")

func f(left, right chan int) { left <- 1 + <-right }

func main() {
	flag.Parse()
	leftmost := make(chan int)
	var left, right chan int = nil, leftmost
	for i := 0; i < *ngoroutine; i++ {
		left, right = right, make(chan int)
		go f(left, right)
	}
	right <- 0      // bang!
	x := <-leftmost // wait for completion
	fmt.Println(x)  // 100000, ongeveer 1,5 s
}
  • 在多核心上並行計算
    假設我們有NCPU個CPU核心:const NCPU = 4 //對應一個四核處理器 然後我們想把計算量分成NCPU個部分,每一個部分都和其他部分並行運行。
func DoAll(){
    sem := make(chan int, NCPU) // Buffering optional but sensible
    for i := 0; i < NCPU; i++ {
        go DoPart(sem)
    }
    // Drain the channel sem, waiting for NCPU tasks to complete
    for i := 0; i < NCPU; i++ {
        <-sem // wait for one task to complete
    }
    // All done.
}

func DoPart(sem chan int) {
    // do the part of the computation
    sem <-1 // signal that this piece is done
}

func main() {
    runtime.GOMAXPROCS(NCPU) // runtime.GOMAXPROCS = NCPU
    DoAll()
}
DoAll()函數創建了一個sem通道,每個並行計算都將在對其發送完成信號;在一個 for 循環中NCPU個協程被啓動了,每個協程會承擔1/NCPU的工作量。每一個DoPart()協程都會向sem通道發送完成信號。
DoAll()會在 for 循環中等待NCPU個協程完成:sem通道就像一個信號量,這份代碼展示了一個經典的信號量模式。
在以上運行模型中,您還需將GOMAXPROCS設置爲NCPU
  • 並行化大量數據的計算
    假設我們需要處理一些數量巨大且互不相關的數據項,它們從一個in通道被傳遞進來,當我們處理完以後又要將它們放入另一個out通道,就像一個工廠流水線一樣。處理每個數據項也可能包含許多步驟:Preprocess(預處理) / StepA(步驟A) / StepB(步驟B) / … / PostProcess(後處理)
    一個典型的用於解決按順序執行每個步驟的順序流水線算法可以寫成下面這樣:
func SerialProcessData(in <-chan *Data, out chan<- *Data) {
    for data := range in {
        tmpA := PreprocessData(data)
        tmpB := ProcessStepA(tmpA)
        tmpC := ProcessStepB(tmpB)
        out <- PostProcessData(tmpC)
    }
}
一次只執行一個步驟,並且按順序處理每個項目:在第1個項目沒有被PostProcess並放入out通道之前絕不會處理第2個項目。

如果你仔細想想,你很快就會發現這將會造成巨大的時間浪費。

一個更高效的計算方式是讓每一個處理步驟作爲一個協程獨立工作。每一個步驟從上一步的輸出通道中獲得輸入數據。這種方式僅有極少數時間會被浪費,而大部分時間所有的步驟都在一直執行中:
func ParallelProcessData (in <-chan *Data, out chan<- *Data) {
    // make channels:
    preOut := make(chan *Data, 100)
    stepAOut := make(chan *Data, 100)
    stepBOut := make(chan *Data, 100)
    stepCOut := make(chan *Data, 100)
    // start parallel computations:
    go PreprocessData(in, preOut)
    go ProcessStepA(preOut,StepAOut)
    go ProcessStepB(StepAOut,StepBOut)
    go ProcessStepC(StepBOut,StepCOut)
    go PostProcessData(StepCOut,out)
}   
通道的緩衝區大小可以用來進一步優化整個過程。
  • 漏桶算法
    考慮以下的客戶端-服務器結構:客戶端協程執行一個無限循環從某個源頭(也許是網絡)接收數據;數據讀取到Buffer類型的緩衝區。爲了避免分配過多的緩衝區以及釋放緩衝區,它保留了一份空閒緩衝區列表,並且使用一個緩衝通道來表示這個列表:var freeList = make(chan *Buffer,100)
    這個可重用的緩衝區隊列(freeList)與服務器是共享的。 當接收數據時,客戶端嘗試從freeList獲取緩衝區; 但如果此時通道爲空,則會分配新的緩衝區。 一旦消息被加載後,它將被髮送到服務器上的serverChan通道:
var serverChan = make(chan *Buffer)
以下是客戶端的算法代碼:

 func client() {
    for {
        var b *Buffer
        // Grab a buffer if available; allocate if not 
        select {
            case b = <-freeList:
                // Got one; nothing more to do
            default:
                // None free, so allocate a new one
                b = new(Buffer)
        }
        loadInto(b)         // Read next message from the network
        serverChan <- b     // Send to server
        
    }
 }
 服務器的循環則接收每一條來自客戶端的消息並處理它,之後嘗試將緩衝返回給共享的空閒緩衝區:
 func server() {
    for {
        b := <-serverChan       // Wait for work.
        process(b)
        // Reuse buffer if there's room.
        select {
            case freeList <- b:
                // Reuse buffer if free slot on freeList; nothing more to do
            default:
                // Free list full, just carry on: the buffer is 'dropped'
        }
    }
}
但是這種方法在freeList通道已滿的時候是行不通的,因爲無法放入空閒freeList通道的緩衝區會被“丟到地上”由垃圾收集器回收(故名:漏桶算法)
  • 對Go協程進行基準測試
    在此我們將其應用到一個用協程向通道寫入整數再讀出的實例中。這個函數將通過testing.Benchmark調用N次(例如:N = 1,000,000),BenchMarkResult有一個String()方法來輸出其結果。N的值將由gotest來判斷並取得一個足夠大的數字,以獲得合理的基準測試結果。當然同樣的基準測試方法也適用於普通函數。
    如果你想排除指定部分的代碼或者更具體的指定要測試的部分,可以使用testing.B.startTimer()和testing.B.stopTimer()來開始或結束計時器。基準測試只有在所有的測試通過後才能運行!
package main

import (
	"fmt"
	"testing"
)

func main() {
	fmt.Println(" sync", testing.Benchmark(BenchmarkChannelSync).String())
	fmt.Println("buffered", testing.Benchmark(BenchmarkChannelBuffered).String())
}

func BenchmarkChannelSync(b *testing.B) {
	ch := make(chan int)
	go func() {
		for i := 0; i < b.N; i++ {
			ch <- i
		}
		close(ch)
	}()
	for range ch {
	}
}

func BenchmarkChannelBuffered(b *testing.B) {
	ch := make(chan int, 128)
	go func() {
		for i := 0; i < b.N; i++ {
			ch <- i
		}
		close(ch)
	}()
	for range ch {
	}
}
  • 使用通道併發訪問對象
    爲了保護對象被併發訪問修改,我們可以使用協程在後臺順序執行匿名函數來替代使用同步互斥鎖。在下面的程序中我們有一個類型Person其中包含一個字段chF,這是一個用於存放匿名函數的通道。
    這個結構在構造函數NewPerson()中初始化的同時會啓動一個後臺協程backend()。backend()方法會在一個無限循環中執行chF中放置的所有函數,有效的將它們序列化從而提供了安全的併發訪問。更改和讀取salary的方法會通過將一個匿名函數寫入chF通道中,然後讓backend()按順序執行以達到其目的。需注意的是Salary方法創建的閉包函數是如何將fChan通道包含在其中的。
package main

import (
	"fmt"
	"strconv"
)

type Person struct {
	Name   string
	salary float64
	chF    chan func()
}

func NewPerson(name string, salary float64) *Person {
	p := &Person{name, salary, make(chan func())}
	go p.backend()
	return p
}

func (p *Person) backend() {
	for f := range p.chF {
		f()
	}
}

// Set salary.
func (p *Person) SetSalary(sal float64) {
	p.chF <- func() { p.salary = sal }
}

// Retrieve salary.
func (p *Person) Salary() float64 {
	fChan := make(chan float64)
	p.chF <- func() { fChan <- p.salary }
	return <-fChan
}

func (p *Person) String() string {
	return "Person - name is: " + p.Name + " - salary is: " + strconv.FormatFloat(p.Salary(), 'f', 2, 64)
}

func main() {
	bs := NewPerson("Smith Bill", 2500.5)
	fmt.Println(bs)
	bs.SetSalary(4000.25)
	fmt.Println("Salary changed:")
	fmt.Println(bs)
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章