1. 程式人生 > >Golang Goroutine 和 Channel 的使用

Golang Goroutine 和 Channel 的使用

參考閱讀:

什麼是 Goroutine

Goroutines 是與其他函式或方法同時執行的函式或方法。Goroutines可以被認為是輕量級執行緒。 與執行緒相比,建立Goroutine的成本很小。因此,Go應用程式通常會同時執行數千個Goroutines。

Goroutine 的優勢

1)與執行緒相比,Goroutines非常便宜。 它們的堆疊大小隻有幾kb,堆疊可以根據應用程式的需要增長和縮小,而線上程的情況下,堆疊大小必須指定並且是固定的。
2)Goroutines被多路複用到較少數量的OS執行緒。程式中可能只有一個執行緒有數千個Goroutines。 如果該執行緒中的任何Goroutine阻止等待使用者輸入,則建立另一個OS執行緒,並將剩餘的Goroutines移動到新的OS執行緒。 所有這些都由執行時處理,我們作為程式設計師從這些複雜的細節中抽象出來,並給出一個乾淨的API來處理併發。
3)Goroutines使用Channel進行交流。設計Channel可防止使用Goroutines訪問共享記憶體時發生競爭條件。Channel可以被認為是Goroutines通訊的管道。

如何建立一個 Goroutine

package main

import (
“fmt”
)

func hello() {
fmt.Println(“Hello world goroutine”)
}
func main() {
go hello()
fmt.Println(“main function”)
}

Goroutine 兩個主要屬性

1)當一個新的Goroutine啟動時,goroutine呼叫立即返回。 與函式不同,控制元件不會等待Goroutine完成執行。 在Goroutine呼叫之後,控制元件立即返回到下一行程式碼,並忽略Goroutine的任何返回值。
2)主要的Goroutine應該執行任何其他Goroutines執行。 如果主要的Goroutine終止,則該程式將被終止,並且沒有其他Goroutine將執行。

package main

import (
“fmt”
“time”
)

func hello() {
fmt.Println(“Hello world goroutine”)
}
func main() {
go hello()
time.Sleep(1 * time.Second)
fmt.Println(“main function”)
}

通過 time.sleep 可以模擬等待其他routine執行結束再退出的情形,實際情況下我們可以使用channel。channel 可用於阻擋主Goroutine,直到所有其他Goroutines完成執行。

Goroutine 的通訊管道 Channel

通道可以被認為是Goroutines通訊的管道。類似於水在管道中從一端流向另一端的方式,資料可以從一端傳送,另一端使用通道接收。

如何定義Channel
每個通道都有一個與之關聯的型別。此型別是允許通道傳輸的資料型別。不允許使用該頻道傳輸其他型別。
Channel的零值為nil。 nil通道沒有任何用處,因此必須使用類似於map和slice的make來定義通道。

package main

import “fmt”

func main() {
var a chan int
if a == nil {
fmt.Println(“channel a is nil, going to define it”)
a = make(chan int)
fmt.Printf(“Type of a is %T”, a)
}
}

除了上述定義方式,下一種方式也可以。

a := make(chan int)

既然Channel可以從一端寫入資料,從另一端讀取資料,具體是如何做到呢?

data := <- a // read from channel a
a <- data // write to channel a

注意點:

預設情況下,對通道的傳送和接收是阻止的。 這是什麼意思? 當資料傳送到通道時,控制在傳送語句中被阻止,直到其他Goroutine從該通道讀取。 類似地,當從通道讀取資料時,讀取被阻止,直到一些Goroutine將資料寫入該通道。

通道的這種屬性有助於Goroutines有效地進行通訊,而無需使用在其他程式語言中非常常見的顯式鎖或條件變數。

Sends and receives to a channel are blocking by default. What does this mean? When a data is sent to a channel, the control is blocked in the send statement until some other Goroutine reads from that channel. Similarly when data is read from a channel, the read is blocked until some Goroutine writes data to that channel.

This property of channels is what helps Goroutines communicate effectively without the use of explicit locks or conditional variables that are quite common in other programming languages.

來一個示例來看看

package main

import (
“fmt”
)

func hello(done chan bool) {
fmt.Println(“Hello world goroutine”)
done <- true
}
func main() {
done := make(chan bool)
go hello(done)
<-done
fmt.Println(“main function”)
}

這裡主函式main中建立了一個型別為bool的channel,在啟動一個goroutine後,立即從done中讀取資料,這樣就會阻塞main函式流程,直到done中有資料寫入,即hello函式的執行,當done收到資料後,main函式的控制流程才會繼續往下面走。為了更加詳細地理解這個過程,請看如下示例:

package main

import (
“fmt”
“time”
)

func hello(done chan bool) {
fmt.Println(“hello go routine is going to sleep”)
time.Sleep(4 * time.Second)
fmt.Println(“hello go routine awake and going to write to done”)
done <- true
}
func main() {
done := make(chan bool)
fmt.Println(“Main going to call hello go goroutine”)
go hello(done)
<-done
fmt.Println(“Main received data”)
}

輸出內容如下:

Main going to call hello go goroutine
hello go routine is going to sleep
hello go routine awake and going to write to done
Main received data

從輸出結果可以得出結論,<-done 確實在hello sleep 4 秒的時候在等待,直到獲取到管道中的資料。

Channel 死鎖

使用Channel時要考慮的一個重要因素是死鎖。 如果Goroutine正在通道上傳送資料,那麼預計其他一些Goroutine應該接收資料。 如果沒有發生這種情況,程式將在執行時因死鎖而發生Panic。

同樣,如果Goroutine正在等待從一個Channel接收資料,那麼其他一些Goroutine預計會在該Channel上寫入資料,否則程式將會出現Panic。如下這種情況就會Panic

package main

func main() {
ch := make(chan int)
ch <- 5
}

單向Channel

到目前為止我們討論的所有Channel都是雙向的,即資料可以在它們上傳送和接收。 也可以建立單向Channel,即僅傳送或接收資料的Channel。如下是一個單向寫入,但不允許讀取的Channel

package main

import “fmt”

func sendData(sendch chan<- int) {
sendch <- 10
}

func main() {
sendch := make(chan<- int)
go sendData(sendch)
fmt.Println(<-sendch)
}

執行輸出如下:

invalid operation: <-sendch (receive from send-only type chan<- int)

這個例子還告訴我們,寫一個只能寫入卻不能讀取的Channel沒什麼意義。

這是Channel轉換投入使用的地方。可以將雙向Channel轉換為僅傳送或僅接收Channel,反之亦然。如下示例:

package main

import “fmt”

func sendData(sendch chan<- int) {
sendch <- 10
}

func main() {
sendch := make(chan int)
go sendData(sendch)
fmt.Println(<-sendch)
}

輸出: 10

關閉Channel 和 Channel 在迴圈體中的輪詢

Channel 的資料傳送者能夠關閉Channel以通知接收者不再在該Channel上傳送資料。Receivers可以在從Channel接收資料時使用附加變數來檢查通道是否已關閉。

v, ok := <- ch

在上面的語句中,如果通過成功的傳送操作接收到該值,則確定為真。 如果ok為false,則意味著我們正在從封閉的Channel中讀取。 從封閉Channel讀取的值將是通道型別的零值。 例如,如果通道是int通道,則從閉合Channel接收的值將為0。

package main

import (
“fmt”
)

func producer(chnl chan int) {
for i := 0; i < 10; i++ {
chnl <- i
}
close(chnl)
}
func main() {
ch := make(chan int)
go producer(ch)
for {
v, ok := <-ch
if ok == false {
break
}
fmt.Println("Received ", v, ok)
}
}

這個示例比較簡單,從product方法中寫入channel結束後,直接關閉channel,由於main中的for迴圈體中不停地讀取,當收到close訊號時結束讀取。

值得注意的是這裡的迴圈結束條件,通過獲取Channel的close訊號,那麼是否有更簡潔的寫法呢?

package main

import (
“fmt”
)

func producer(chnl chan int) {
for i := 0; i < 10; i++ {
fmt.Println("Inpute ", i)
chnl <- i
}
fmt.Println(“Channel will Close.”)
close(chnl)
}
func main() {
ch := make(chan int)
go producer(ch)
for v := range ch {
fmt.Println("Received ",v)
}
}

輸出:

Inpute 0
Inpute 1
Received 0
Received 1
Inpute 2
Inpute 3
Received 2
Received 3
Inpute 4
Inpute 5
Received 4
Received 5
Inpute 6
Inpute 7
Received 6
Received 7
Inpute 8
Inpute 9
Received 8
Received 9
Channel will Close.

for range 中在從Channel中讀取資料,但Channel關閉的時候,迴圈自動就退出了。這裡我們可以節省判斷Channel是否被關閉的訊號。