一文弄清GO語言的併發基礎

相信很多讀者和我一樣,也都是衝着 Go 大肆宣揚的高併發而入坑,從源碼的角度來看,協程goroutine本質是由谷歌實現的超級“線程池”。每個goroutine大約 4-5KB的棧內存消耗的確更加符合目前web服務的場景,和由於實現機制和輕量級的開銷,Go的高併發性的確有目共睹。

goroutine

在我們使用JAVA或者C編寫網絡程序時,基本都是用一個線程來處理一個http請求, 但是這樣的方式對於資源的利用率不高。而Go語言就實現了這樣一種輕量級線程的機制,GO語言在底層封裝了所有的系統調用,自己實現了一個調度器,這種設計在操作系統的代碼中非常多見,比如現代的操作系統基本都會封裝一個軟件的Timer,同時可以提供上萬個軟Timer同時工作,而這只是基於數量很少的硬件timer實現的,而GO語言中的併發也是如此,他是基於線程的調度池,這種調度的單元在Go語言中被稱爲goroutine

一、goroutine的啓動

Go 程序中使用 go 關鍵字爲一個函數創建一個 goroutinegoroutine 必定對應一個函數。爲一個普通函數創建 goroutine 的方式如下:

go 函數名( 參數列表 )

當然也可以使用匿名函數來創建goroutine方式如下:

go func( 參數列表 ){
    函數體
}( 調用參數列表 )
注意使用匿名來啓動goroutine是可以和閉包結合的

下面我們來舉例說明,在下面的例程當中我們先通過running函數來創建一個goroutine1,並使用匿名函數來創造了另一個goroutine2,並且在最後使用sleep功能阻塞主函數,給兩個子goroutine以運行的機會。

package main

import (
	"fmt"
	"time"
)

func running() {
	var times int
	for {// 構建一個無限循環
		times++
		fmt.Println("this is runing function's tick", times)//從筆者的機器上看runing函數中的ticks累加到300左右
	}
}
func main() {

	go running() //通過普通函數調用goroutine
	go func() {  //通過匿名函數調用goroutine
		var times int //注意這個times變量其實和匿名函數共同構成閉包
		for {
			times++
			fmt.Println("this is anonymous function's tick ", times)//從筆者的機器上看runing函數中的ticks累加到300左右
		}
	}()
	time.Sleep(time.Millisecond)//在本例中使用休眠的方式來阻塞main函數創建的主goroutine

}

二、深入理解goroutine的調度機制

1.例程的執行時序

剛剛的例程中程序運行的時序圖如下,這個例子中,G程序在啓動時會創建一個main() 函數的goroutine也稱主goroutine。在 main() 函數執行到 go running 語句時,歸屬於 running() 函數的 goroutine 被創建,執行到go func 語句時,歸屬於匿名函數的 goroutine 被創建,這兩個goroutine實際都是由主goroutine創建的,在主goroutine未放棄CPU的情況下,子goroutine不會被調度執行,直到執行sleep(time.Millisecond)goroutine被阻塞,處於就緒態的子goroutine纔有執行機會,而當sleep到時後,主 goroutine退出運行態,此時兩個子Goroutine也隨之退出。具體如下圖:

 

2.goroutine的調度策略

這裏再爲各位讀者補充一下必要的操作系統知識,與一般操作系統一樣,協程也將運行狀態抽象歸爲三種:

就緒(Ready):該goroutine在就緒列表中,只等待CPU

運行(Running):該goroutine正在執行。

阻塞(Blocked):該goroutine不在就緒列表中。包含掛起、延時、等待信號量、等待事件等等。

下次我們再來探討一下GO語言中是不是隻有通過time.sleep才能讓子的goroutine得以執行。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章