golang學習筆記(二)—— 深刻golang中的協程


小白一枚,最近在研究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」。學習

一、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
    3
    2
  • 第二次
    3
    2
    1
  • 第三次
    3
    1
    2

咱們發現由於協程是併發執行的,咱們沒法肯定其調用的順序,所以 每次的調用主函數的返回結果都是不肯定的。

從協程的上述例子中,咱們能夠看出使用協程的時候必須還要考慮兩個問題:

  • 如何控制協程的調用順序,特別是當不一樣的協程同時訪問同一個資源。
  • 如何實現不一樣協程間的通訊

問題1,能夠經過sync的同步鎖來實現,問題2,go中提供了channel來實現不一樣協程間的通訊。

二、go中協程的sync同步鎖

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

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來代替。

注意:通道能夠被顯式的關閉,當須要告訴接受者不會種子提供新的值的時候,就須要關閉通道。

四、go中的range

上面咱們也講到要及時的關閉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循環也就自動中止了。

五、go中的select切換協程

從不一樣的併發執行過程當中獲取值能夠經過關鍵字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個數據。且由於沒有作同步控制,所以運行幾回後的輸出結果是不相同的。

六、go中帶緩存的channel

前面講到的都是不帶緩存的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中的協程調度。go中的runtime包,提供了調度器的功能。runtime包提供瞭如下幾個方法:

  • Gosched:讓當前線程讓出 cpu 以讓其它線程運行,它不會掛起當前線程,所以當前線程將來會繼續執行
  • NumCPU:返回當前系統的 CPU 核數量
  • GOMAXPROCS:設置最大的可同時使用的 CPU 核數
  • Goexit:退出當前 goroutine(可是defer語句會照常執行)
  • NumGoroutine:返回正在執行和排隊的任務總數
  • GOOS:目標操做系統

對於多核CPU的機器,go能夠顯示的指定編譯器將go的協程調度到多個CPU上運行

import "runtime"
...
cpuNum:=runtime.NumCPU;
runtime.GOMAXPROCS(cpuNum)

來聊聊GO中的調度原理,首先定義如下模型的概念:

M:內核中的線程的數目
G:go中的協程,併發的最小單元,在go中經過go關鍵字來建立
P:處理器,即協程G的上下文,每一個P會維護一個本地的協程隊列。

接着來看解釋GO中協程調度的經典圖:

1141545827812_ pic_hd

咱們來解釋上圖:

  • P是處理器的個數,咱們常常將調度器的GOMAXPROCS設置成CPU的個數,所以這裏P通常來講是機器CPU的個數。
  • M是線程,在P處理器上關聯一個線程,P和M的一組配對組成了局部的協程隊列
  • G就是協程,須要被添加到由P和M組成的局部隊列中依次處理
  • 除了局部的協程外,在全局還維護了一個協程隊列。
  • 若是局部協程隊列中處理完了全部隊列,且沒有新隊列,那麼M線程會取消對於CPU的佔用,M線程進入休眠
相關文章
相關標籤/搜索