Go程式設計語言(三)

來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。

本文譯自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. 著作權.

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在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.