上一篇文章文章主要學習了Go語言中的介面、反射以及錯誤和異常處理。本篇文章主要學習Go語言的協程,當然也是GO語言基礎的最後一篇。
goroutine:
goroutine是Go並行設計的核心,也是這門語言的精髓體現。goroutine這個關鍵字就是協程,但是它比線程更小。說起線程,大家可能都不陌生。線程,是程式執行的最小單元。一個標準的線程由線程ID,當前指令指標,寄存器集合和堆棧組成。另外,線程是進程中的一個實體,是被系統獨立調度和指派的基本單位,線程自己不擁有系統資源,只擁有一點兒在運行中必不可少的資源,但它可與同屬一個進程的其它線程共用進程所擁有的全部資源。
現在Go語言運用協程這一比線程更小的執行單元,十幾個goroutine可能體現在底層就是五六個線程,Go語言內部幫你實現了這些goroutine之間的記憶體共用。執行goroutine只需極少的棧記憶體(大概是4~5KB),當然會根據相應的資料伸縮。也正因為如此,可同時運行成千上萬個並發任務。goroutine比thread更易用、更高效、更輕便。
goroutine是通過Go的runtime管理的一個線程管理器。goroutine的作用就是一個普通的函數。以下是協程的寫法以及結果測試:
測試 - 1
測試 - 2
測試 - 3
理論上來說:多個goroutine運行在同一個進程裡面,共用記憶體資料,不過設計上我們要遵循:不要通過共用來通訊,而要通過通訊來共用。goroutine運行在相同的地址空間,因此訪問共用記憶體必須做好同步。那麼goroutine之間如何進行資料的通訊呢,Go提供了一個很好的通訊機制:channel。
channel是Go中的一個核心類型,你可以把它看成一個管道,通過它並發核心單元就可以發送或者接收資料進行通訊。
首先,channel必須先建立再使用,另外它的操作符是箭頭 <-
Channel類型的定義格式(有三種寫法)如下:
ChannelType = ( "chan" | "chan" "<-" | "<-" "chan" ) 資料類型
其中,這裡的 <- 代表的是channel方向。如果沒有指定方向,那麼Channel就是雙向,這樣就意味可以接收資料,也可以發送資料。
(前面說了可以把channel看成一個管道,既然是管道那麼就有流向)。也就是下面三種寫法:
chan T // 可以接收和發送類型為 T 的資料
chan<- float64 // 只可以用來發送 float64 類型的資料
<-chan int // 只可以用來接收 int 類型的資料
另外:
<-總是優先和最左邊的類型結合。以下是幾種組合寫法:
chan<- chan int // 等價 chan<- (chan int)
chan<- <-chan int // 等價 chan<- (<-chan int)
<-chan <-chan int // 等價 <-chan (<-chan int)
容量(capacity):
使用make也可以初始化Channel,並且可以設定channel的容量:
這裡的容量可以這樣理解,就是channel可以儲存多少個元素,指定channel的緩衝大小、另外一個nil channel不會通訊。
寫法:
ch := make(chan type, value)
當 value = 0 時,也就說明channel 是無緩衝阻塞讀寫的;
當 value > 0 時,channel 有緩衝、是非阻塞的,直到寫滿 value 個元素才阻塞寫入。例如:
容量
以上代碼不變,當我們把容量設定為1,就會出現如下問題:
容量不足
Range和Close
range,這個關鍵字可以像操作slice或者map一樣,去操作緩衝類型的channel;
close,這個關鍵字主要是用來關閉channel。關閉channel之後就無法再發送任何資料。
rang
上面的代碼都是只有一個channel的情況,那麼如果存在多個channel的時候,我們該如何操作呢?
Go裡面提供了一個關鍵字select,通過select可以監聽channel上的資料流動。
它類似switch,但是只是用來處理通訊(communication)操作。它的case可以是send語句(發送),也可以是receive語句(接收),亦或者default。
default就是當監聽的channel都沒有準備好的時候,預設執行
select預設是阻塞的,只有當監聽的channel中有發送或接收可以進行時才會運行,當多個channel都準備好的時候,select是隨機的選擇一個執行的。
代碼如下:
func main() {
c := make(chan int)
quit := make(chan int)
go func() {
for i := 0; i < 3; i++ {
fmt.Println("<-c == ",<-c)
}
quit <- 0
}()
testSelect(c, quit)
}
func testSelect(c, quit chan int) {
x, y := 1, 1
for {
select {
case c <- x:
x, y = y, x + y
fmt.Println("x == ",x)
fmt.Println("y+x == ",y+x)
case <- quit:
fmt.Println("quit")
return
}
}
}
逾時處理:timeout
select有很重要的一個應用就是逾時處理。如果select中沒有case需要處理,select語句就會一直阻塞著。
這時候我們可能就需要一個逾時操作,用來處理逾時以避免整個程式進入阻塞。Go語言的逾時處理是使用的timeout關鍵字
以下是複現逾時的例子,我分了兩種情況:
下面是第二種,複現逾時的情景:
複現逾時
下面是收集了一些runtime包中關於處理goroutine的幾個函數:
A: Goexit 這個函數的意思指:退出當前執行的goroutine,但是defer函數還會繼續調用
B: Gosched 這個函數的意思指:讓出當前goroutine的執行許可權,調度器安排其他等待的任務運行,並在下次某個時候從該位置恢複執行。
C: NumCPU 這個函數的意思指:返回 CPU 核心數量
D: NumGoroutine 這個函數的意思指:返回正在執行和排隊的任務總數
E: GOMAXPROCS 這個函數的意思指:用來設定可以並行計算的CPU核心數的最大值,並返回之前的值。
結語:
關於Go語言的基礎內容,基本上就寫完了。也算是這一階段的學習筆記與總結。
GoLang在學習的過程中,個人最深刻的體會就是對記憶體的超嚴格控制管理、文法簡潔精悍、更小更精準的協程執行單元、函數多傳回值等等特點。
雖然網上對這麼語言褒貶不一,但還是希望這門語言可以發展的越來越好。
如果這篇文章對你有協助,希望各位看官留下寶貴的star,謝謝。
Ps:著作權歸作者所有,轉載請註明作者, 商業轉載請聯絡作者獲得授權,非商業轉載請註明出處(開頭或結尾請添加轉載出處,添加原文url地址),文章請勿濫用,也希望大家尊重筆者的勞動成果