小白一枚,最近在研究golang,記錄本身學習過程當中的一些筆記,以及本身的理解。git
- go中協程的實現
- go中協程的sync同步鎖
- go中信道channel
- go中的range
- go中的select切換協程
- go中帶緩存的channel
- go中協程調度
原文的地址爲:https://github.com/forthealll...github
歡迎stargolang
介紹go中的協程以前,首先看如下go中的defer函數,defer函數不是普通的函數,defer函數會在普通函數返回以後執行。defer函數中能夠釋放函數內部變量、關閉數據庫鏈接等等操做,舉例來講:數據庫
func print(){ fmt.Println(2); } func main() { defer print(); fmt.Println(1); }
上述的例子中先輸出1後輸出2,說明defer確實是在普通函數調用結束以後執行的。緩存
go中使用協程的方式來處理併發,協程能夠理解成更小的線程,佔用空間小且線程上下文切換的成本少。併發
能夠再爲具體的描述如下協程的好處,協程比線程更加輕量,使用4K棧的內存就能夠建立它們,能夠用很小的內存佔用就能夠處理大量的任務。函數
在go中,攜程是經過go關鍵字來調用,從關鍵字能夠看出,golang的一個十分重要的特色就是協程,有句話叫「協程在手,說go就go」。學習
下面咱們來看一個例子:spa
func printOne(){ fmt.Println(1); } func printTwo(){ fmt.Println(2); } func printThree(){ fmt.Println(3); } func main() { go printOne(); go printTwo(); go printThree(); }
執行上述的main函數,咱們發現並無像咱們想的那樣輸出有123的輸出,緣由在於雖然協程是併發的,可是若是在協程調用前退出了調用協程的函數後,協程會隨着程序的消亡而消亡。操作系統
所以咱們能夠在main函數中,將主函數掛起,增長等待協程調用的事件。
func main() { go printOne(); go printTwo(); go printThree(); time.Sleep(5 * 1e9); }
這樣會有相應的go關鍵字修飾的協程函數的調用。咱們來看分別執行3次的結果。
咱們發現由於協程是併發執行的,咱們沒法肯定其調用的順序,所以 每次的調用主函數的返回結果都是不肯定的。
從協程的上述例子中,咱們能夠看出使用協程的時候必須還要考慮兩個問題:
問題1,能夠經過sync的同步鎖來實現,問題2,go中提供了channel來實現不一樣協程間的通訊。
go中sync包提供了2個鎖,互斥鎖sync.Mutex和讀寫鎖sync.RWMutex.咱們用互斥鎖來解決上述的同步問題,改寫上述的例子:
func printOne(m *sync.Mutex){ m.Lock(); fmt.Println(1); defer m.Unlock(); } func printTwo(m *sync.Mutex){ m.Lock(); fmt.Println(2); defer m.Unlock(); } func printThree(m *sync.Mutex){ m.Lock(); fmt.Println(3); defer m.Unlock(); } func main() { m:= new(sync.Mutex); go printOne(m); go printTwo(m); go printThree(m); time.Sleep(5 * 1e9); }
經過互斥鎖,能夠發現每次運行,確實都依次輸出了1,2,3
go中有一種特殊的類型通道channel,能夠經過channel來發送類型化的數據,實如今協程之間的通訊,經過通道的通訊方式也保證了同步性。
channel的聲明方式很簡單:
var ch1 chan string ch1 = make(chan string)
咱們用ch表示通道,通道的符號包括了流向通道(發送): ch <- int1 和從通道流出(接收) int2 = <- ch。
同時go中也支持聲明單向通道:
var ch1 chan int //普通的channel var ch2 chan <- int //只用於寫int數據 var ch3 <- chan int //只用於讀int數據
上述定義的都是不帶緩存區,或者說長度爲1的channel,這種channel的特色就是:
一旦有數據被放入channel,那麼該數據必須被取走才能讓另外一條數據放入,這就是同步的channel,channel的發送者和接受者在同一時間只交流一條數據,而後必須等待另外一邊完成相應的發送和接受動做。
咱們仍是用上述的輸出123的例子,用同步channel來實現同步的輸出。
func printOne(cs chan int){ fmt.Println(1); cs <- 1 } func printTwo(cs chan int){ <-cs fmt.Println(2); defer close(cs); } func main() { cs := make(chan int); go printOne(cs); go printTwo(cs); time.Sleep(5 * 1e9); }
上述的例子中會依次輸出12,這樣咱們經過同步channel的方式實現了同步的輸出。
咱們前面講到用爲了等待go協程執行完成,咱們在main函數中用time.sleep來掛起主函數,其實main函數自己也能夠當作一個協程,若是使用channel,就不用在main函數中用time.sleep來掛起。
咱們改寫上述的例子:
func printOne(cs chan int){ fmt.Println(1); cs <- 1 } func main() { cs := make(chan int); go printOne(cs); <-cs; close(cs); }
上述的例子中,會輸出 1 ,咱們並無在主函數中經過time.sleep的方式來掛起,轉而用一個等待寫入的channel來代替。
注意:通道能夠被顯式的關閉,當須要告訴接受者不會種子提供新的值的時候,就須要關閉通道。
上面咱們也講到要及時的關閉channel,可是持續的訪問數據源並檢查channel是否已經關閉,並不高效。go中提供了range關鍵字。
range關鍵字在使用channel的時候,會自動等待channel的動做一直到channel關閉。通俗點將就是能夠channel能夠自動開關。
一樣的來舉例:
func input(cs chan int,count int){ for i:=1;i<=count;i++ { cs <- i } } func output(cs chan int){ for s:= range cs { fmt.Println(s); } } func main() { cs := make(chan int); go input(cs,5); go output(cs); time.Sleep(3*1e9) }
上述的例子會依次的輸出1,2,3,4,5. 經過使用range關鍵字,當channel被關閉時,接受者的for循環也就自動中止了。
從不一樣的併發執行過程當中獲取值能夠經過關鍵字select來完成,它和switch控制語句很是類似,也被稱爲通訊開關。
首先要明確select作了什麼??
select中存在着一種輪詢機制,select監聽進入通道的數據,也能夠是通道發送值的時候,監聽到相應的行爲後就執行case裏面的操做。
select的聲明:
select { case u:= <- ch1: ... case v:= <- ch2; ... }
一樣的來看一下具體使用select的例子:
func channel1(cs chan int,count int){ for i:=1;i<=count;i++ { cs <- i } } func channel2(cs chan int,count int){ for i:=1;i<=count;i++ { cs <- i } } func selectTest(cs1 ,cs2 chan int){ for i:=1;i<10;i++ { select { case u:=<-cs1: fmt.Println(u); case v:=<-cs2: fmt.Println(v); } } } func main() { cs1 := make(chan int); cs2 := make(chan int); go channel1(cs1,5); go channel2(cs2,3); go selectTest(cs1,cs2); time.Sleep(3*1e9) } 輸出結果爲:1,2,1,2,3,3,4,5 總共8個數據。且由於沒有作同步控制,所以運行幾回後的輸出結果是不相同的。
前面講到的都是不帶緩存的channel或者說長度爲1的channel,實際上channel也是能夠帶緩存的,咱們能夠在聲明的時候執行channel的長度。
ch = make(chan string,3)
好比上述的例子中,指定了ch這個channel的長度爲3,長度不爲1的channel,就能夠稱之爲帶緩存的channel.
帶緩存的channel能夠連續寫入,直到長度佔滿爲止。
ch <- 1 ch <- 2 ch <- 3
講到併發,就要提到go中的協程調度。go中的runtime包,提供了調度器的功能。runtime包提供瞭如下幾個方法:
對於多核CPU的機器,go能夠顯示的指定編譯器將go的協程調度到多個CPU上運行
import "runtime" ... cpuNum:=runtime.NumCPU; runtime.GOMAXPROCS(cpuNum)
來聊聊GO中的調度原理,首先定義如下模型的概念:
M:內核中的線程的數目
G:go中的協程,併發的最小單元,在go中經過go關鍵字來建立
P:處理器,即協程G的上下文,每一個P會維護一個本地的協程隊列。
接着來看解釋GO中協程調度的經典圖:
咱們來解釋上圖: