這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
本文譯自Rob Pike的Go語言PPT教程 – "The Go Programming Language Part3(updated June 2011)"。由於該教程的最新更新時間早於Go 1版本發布,因此該PPT中的一些內容與Go 1語言規範略有差異,到時我會在相應的地方做上註解。
第三部分大綱
- 並發與通訊
- Goroutines
- 通道(Channel)
- 並發相關話題
並發與通訊:Goroutines
Goroutines
術語:
對於"並發啟動並執行事物"已經有了好多術語 – 進程、線程、協程(coroutine)、POSIX線程、NPTL線程、輕量級進程…,但這些事物都或多或少有不同。並且Go中的並發與哪種都不甚相同。
因此我們介紹一個新術語:goroutine。
定義
一個Goroutine是一個與其他goroutines運行在同一地址空間的Go函數或方法。一個啟動並執行程式由一個或更多個goroutine組成。
它與線程、協程、進程等不同。它是一個goroutine。
注意:Concurrency與Parallelism是不同的概念。如果你不瞭解它們的不同,查查相關資料吧。
關於並發的問題有許多。我們後續會提及。現在就假設它能按其對外所宣稱的那樣正常工作吧。
啟動一個Goroutine
調用一個函數或方法,然後說go:
func IsReady(what string, minutes int64) {
time.Sleep(minutes * 60*1e9) // Unit is nanosecs.
fmt.Println(what, "is ready")
}
go IsReady("tea", 6)
go IsReady("coffee", 2)
fmt.Println("I'm waiting…")
列印:
I'm waiting… (立即)
coffee is ready (2分鐘後)
tea is ready (6分鐘後)
一些簡單的事實
goroutine的使用代價很低。
當從最外層函數返回,或執行到結尾處時,goroutine退出。
goroutines可以並行地在不同CPU上執行,共用記憶體。
你無需擔心棧大小。
棧
在gccgo中,至少目前goroutines就是pthreads。在6g中,goroutines採用基於線程的多工技術,因此它們的代價更低廉。
無論是上面哪個實現,棧都很小(幾KB),可以根據需要增長。因此goroutines使用很少的記憶體。你可以建立很多goroutines,它們還可以動態擁有很大的棧。
程式員無需考慮棧大小相關話題。在Go中,這種考慮甚至不應該出現。
調度
Goroutine多工系統線程。當一個goroutine執行了一個阻塞的系統調用時,其他goroutine不會不阻塞。
計劃後續實現CPU綁定的goroutines,不過目前用6g如果你想要使用者層層級的並行,你必須設定環境變數GOMAXPROCS或調用runtime.GOMAXPROCS(n)。
GOMAXPROCS告訴運行時調度器有多少個使用者空間goroutine即將同時執行,理想情況下在不同的CPU核上。
*gccgo總是為每個goroutine單獨分配一個線程執行。
並發與通訊:Channels
Go中的Channel
除非兩個goroutine可以通訊,否則它們無法協作。
Go中有一個名為channel的類型,提供通訊和同步能力。
Go中還提供一些特殊的基於channel的控制結構,使得編寫並發程式更加容易。
Channel類型
該類型最簡單形式:
chan elementType
通過這個類型的值,你可以發送和接收elementType類型的元素。
Channel是參考型別,這意味著如果你將一個chan變數賦值給另外一個,則這兩個變數訪問的是相同的channel。同樣,這也意味著可以用make分配一個channel:
var c = make(chan int)
通訊操作符:<-
箭頭指示資料流向。
作為一個二元操作符,<-將值從右側發送到左側的channel中:
c := make(chan int)
c <- 1 // 向c發送1
作為首碼一元操作符,<- 從一個channel中接收資料:
v = <-c // 從c中接收資料,賦值給v
<-c // 接收資料,丟棄
i := <-c // 接收值,用於初始化i
語義
預設情況下,通訊是同步的。(我們後續將討論非同步通訊)。這意味著:
1) A在一個channel上的發送操作會阻塞,直到該channel上有一個接收者就緒。
2) 在一個channel上到的接收操作會阻塞,直到該channel上有一個寄件者就緒。
因此通訊是同步的一種形式:兩個通過channel交換資料的goroutine在通訊的時刻同步。
讓我們泵一些資料吧
func pump(ch chan int) {
for i := 0; ; i++ { ch <- i }
}
ch1 := make(chan int)
go pump(ch1) // pump掛起; 我們運行
fmt.Println(<-ch1) // 列印 0
現在我們啟動一個迴圈接收者:
func suck(ch chan int) {
for { fmt.Println(<-ch) }
}
go suck(ch1) // 大量數字出現
你仍可以溜進去,抓取一個值:
fmt.Println(<-ch1) // 輸出:3141159
返回channel的函數
在前面的例子中,pump像一個產生器,噴湧出值。但在分配channel等方面做了很多工作。讓我們將其打包到一個返回channel的函數中:
func pump() chan int {
ch := make(chan int)
go func() {
for i := 0; ; i++ { ch <- i }
}()
return ch
}
stream := pump()
fmt.Println(<-stream)// 列印 0
"返回channel的函數"是Go中的一個重要的慣用法。
到處都是返回channel的函數
我這裡不再重複那些你可以從其他地方找到的知名例子。這裡有些可以瞭解一下:
1) prime sieve: 在語言規範以及教程中。
2) Doug McIlroy的Power系列論文:http://plan9.bell-labs.com/who/rsc/thread/squint.pdf
這個程式的一個Go版本在測試套件中:http://golang.org/test/chan/powser1.go
Range和Channel
for迴圈的range子句接收channel作為一個運算元,在這種情況下,for迴圈迭代處理從channel接收到的值。我們來重寫pump函數;這裡是suck的重寫,讓它也啟動一個goroutine:
func suck(ch chan int) {
go func() {
for v := range ch { fmt.Println(v) }
}()
}
suck(pump()) // 現在不再阻塞
關閉一個Channel
range是如何知道何時channel上的資料轉送結束了呢?寄件者調用一個內建函數close:
close(ch)
接收者使用"comma ok"測試寄件者是否關閉了channel:
val, ok:= <- ch
當結果為(value, true),說明依然有資料;一旦channel關閉,資料流幹,結果將會是(zero, false)。
在一個Channel上使用Range
在一個channel上使用range,諸如:
for value := range <-ch {
use(value)
}
等價於:
for {
value, ok := <-ch
if !ok {
break
}
use(value)
}
Close
關鍵點:
只有寄件者可以調用close。
只有接收者可以詢問是否channel被關閉了。
只有在擷取值的同時詢問(避免競爭)
只有在有必要通知接收者不會再有資料的時候才調用close。
大多數情況下,不需要用close;它與關閉一個檔案沒有可比性。
不管怎樣,channel是可以記憶體回收的。
Channel的方向性
一個channel變數的最簡單形式是一個非緩衝(同步的)值,該值可以用於進行發送和接收。
一個channel類型可以被指定為只發或只收:
var recvOnly <-chan int
var sendOnly chan<- int
Channel的方向性(2)
所有Channel建立時都是雙向的,但我們可以將它們賦值給帶方向性的channel變數。從型別安全角度考慮,對於函數內的執行個體非常有用:
func sink(ch <-chan int) {
for { <-ch }
}
func source(ch chan<- int) {
for { ch <- 1 }
}
c := make(chan int)//雙向的
go source(c)
go sink(c)
同步的Channel
同步的Channel是非緩衝的。發送動作不會完成,直到一個接收者接收這個值。
c := make(chan int)
go func() {
time.Sleep(60*1e9)
x := <-c
fmt.Println("received", x)
}()
fmt.Println("sending", 10)
c <- 10
fmt.Println("sent", 10)
輸出:
sending 10 (立即發生)
sent 10 (60秒後,這兩行出現)
received 10
非同步Channel
通過告知make緩衝中元素的數量,我們可以建立一個帶緩衝的、非同步channel。
c := make(chan int, 50)
go func() {
time.Sleep(60*1e9)
x := <-c
fmt.Println("received", x)
}()
fmt.Println("sending", 10)
c <- 10
fmt.Println("sent", 10)
輸出:
sending 10 (立刻發生)
sent 10(現在)
received 10 (60秒後)
緩衝不是類型的一部分
注意緩衝的大小甚至其自身都不是channel類型的一部分,只是值的一部分。因此下面的代碼雖危險,但合法:
buf = make(chan int, 1)
unbuf = make(chan int)
buf = unbuf
unbuf = buf
緩衝是一個值的屬性,而不是類型的。
Select
select是Go中的一個控制結構,類似於用於通訊的switch語句。每個case必須是一個通訊操作,要麼是send要麼是receive。
ci, cs := make(chan int), make(chan string)
select {
case v := <-ci:
fmt.Printf("received %d from ci\n", v)
case v := <-cs:
fmt.Printf("received %s from cs\n", v)
}
Select隨機執行一個可啟動並執行case。如果沒有case可運行,它將阻塞,直到有case可運行。一個預設的子句應該總是可啟動並執行。
Select語義
快速一覽:
- 每個case都必須是一個通訊(可能是:=)
- 所有channel運算式都會被求值
- 所有被發送的運算式都會被求值
- 如果任意某個通訊可以進行,它就執行;其他被忽略。
- 如果有多個case都可以運行,Select會隨機公平地選出一個執行。其他不會執行。
- 否則:
– 如果有default子句,則執行該語句。
– 如果沒有default字句,select將阻塞,直到某個通訊可以運行;Go不會重新對channel或值進行求值。
隨機bit產生器
幼稚但很有說明性的例子:
c := make(chan int)
go func() {
for {
fmt.Println(<-c)
}
}()
for {
select {
case c <- 0: //沒有語句,沒有fallthrough
case c <- 1:
}
}
測試可通訊性
一個通訊是否可以進行,而不阻塞?一個帶default字句的select可以告訴我們:
select {
case v := <-ch:
fmt.Println("received", v)
default:
fmt.Println("ch not ready for receive")
}
如果沒有其他case可以運行,那default子句將被執行,因此這對於非阻塞接收是一個慣用法;非阻塞發送顯然也可以這麼做。
逾時
一個通訊可以在一個給定的時間內成功完成嗎?time包包含了after函數:
func After(ns int64) <-chan int64
在指定時間段之後,它向返回的channel中傳遞一個值(目前時間)。
在select中使用它以實現逾時:
select {
case v := <-ch:
fmt.Println("received", v)
case <-time.After(30*1e9):
fmt.Println("timed out after 30 seconds")
}
多工(multiplexing)
channel是原生值,這意味著他們也能通過channel發送。這個屬性使得編寫一個服務類多工器變得十分容易,因為用戶端在提交請求時可一併提供用於回複應答的channel。
chanOfChans := make(chan chan int)
或者更典型的如:
type Reply struct { … }
type Request struct {
arg1, arg2 someType
replyc chan *Reply
}
多工伺服器
type request struct {
a, b int
replyc chan int
}
type binOp func(a, b int) int
func run(op binOp, req *request) {
req.replyc <- op(req.a, req.b)
}
func server(op binOp, service <-chan *request) {
for {
req := <-service // 請求到達這裡
go run(op, req) // 不等op
}
}
啟動伺服器
使用"返回channel的函數"慣用法來為一個新伺服器建立一個channel:
func startServer(op binOp) chan<- *request {
service := make(chan *request)
go server(op, req)
return service
}
adderChan := startServer(
func(a, b int) int { return a + b }
)
用戶端
在教程中有個例子更為詳盡,但這裡是一個變體:
func (r *request) String() string {
return fmt.Sprintf("%d+%d=%d",
r.a, r.b, <-r.replyc)
}
req1 := &request{7, 8, make(chan int)}
req2 := &request{17, 18, make(chan int)}
請求已經就緒,發送它們:
adderChan <- req1
adderChan <- req2
可以以任何順序獲得結果;r.replyc多路分解:
fmt.Println(req2, req1)
停掉
在多工例子中,服務將永遠運行下去。要將其乾淨地停掉,可通過一個channel發送訊號。下面這個server具有相同的功能,但多了一個quit channel:
func server(op binOp, service <-chan *request,
quit <-chan bool) {
for {
select {
case req := <-service:
go run(op, req) // don't wait for it
case <-quit:
return
}
}
}
啟動伺服器
其餘代碼都相似,只是多了個channel:
func startServer(op binOp) (service chan<- *request,
quit chan<- bool) {
service = make(chan *request)
quit = make(chan bool)
go server(op, service, quit)
return service, quit
}
adderChan, quitChan := startServer(
func(a, b int) int { return a + b }
)
停掉:用戶端
只有當準備停掉服務端的時候,用戶端才會受到影響:
req1 := &request{7, 8, make(chan int)}
req2 := &request{17, 18, make(chan int)}
adderChan <- req1
adderChan <- req2
fmt.Println(req2, req1)
所有都完成後,向伺服器發送訊號,讓其退出:
quitChan <- true
鏈
package main
import ("flag"; "fmt")
var nGoroutine = flag.Int("n", 100000, "how many")
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 // 等待完成
fmt.Println(x) // 100000
}
例子:Channel作為緩衝
var freeList = make(chan *Buffer, 100)
var serverChan = make(chan *Buffer)
func server() {
for {
b := <-serverChan // 等待做work
process(b) // 在緩衝中處理請求
select {
case freeList <- b: // 如果有空間,重用緩衝
default: // 否則,丟棄它
}
}
}
func client() {
for {
var b *Buffer
select {
case b = <-freeList: // 如果就緒,抓取一個
default: b = new(Buffer) // 否則,分配一個
}
load(b)// 讀取下一個請求放入b中
serverChan <- b // 將請求發給server.
}
}
並發
並發相關話題
許多並發方面,當然,Go一直在儘力做好它們。諸如Channel發送和接收是原子的。select語句也是縝密定義和實現的等。
但goroutine在共用記憶體中運行,通訊網路可能死結,多線程調試器糟糕透頂等等。
接下來做什嗎?
Go給予你原生的
不要用你在使用C或C++或甚至是Java時的方式去編程。
channel給予你同步和通訊的能力,並且使得它們很強大,但也可以很容易知道你是否可以很好的使用它們。
規則是:
不要通過共用記憶體通訊,相反,通過通訊共用記憶體。
特有的通訊行為保證了同步!
模型
例如,使用一個channel發送資料到一個專職服務goroutine。如果同一時刻只有一個goroutine擁有指向資料的指標,那談不上什麼並發。
這是我們極力推薦的服務端編程模型,至少是對舊的"每個用戶端一個線程"的泛化。它自從20世紀80年代就開始使用了,它工作的很好。
記憶體模型
那關於同步和共用記憶體的令人生厭的細節在:
http://golang.org/doc/go_mem.html
但如果你遵循我們的方法,你很少需要理解那些內容。
2012, bigwhite. 著作權.