goroutine簡介
goroutine是go語言中最為NB的設計,也是其魅力所在,goroutine的本質是協程,是實現并行計算的核心。goroutine使用方式非常的簡單,只需使用go關鍵字即可啟動一個協程,并且它是處于異步方式運行,你不需要等它運行完成以后在執行以后的代碼。GO默認是使用一個CPU核的,除非設置runtime.GOMAXPROCS。
go func()//通過go關鍵字啟動一個協程來運行函數
概念普及
-
并發:
一個cpu上能同時執行多項任務,在很短時間內,cpu來回切換任務執行(在某段很短時間內執行程序a,然后又迅速得切換到程序b去執行),有時間上的重疊(宏觀上是同時的,微觀仍是順序執行),這樣看起來多個任務像是同時執行,這就是并發。 -
并行
當系統有多個CPU(多核)時,每個CPU同一時刻都運行任務,互不搶占自己所在的CPU資源,同時進行,稱為并行。 -
進程
cpu在切換程序的時候,如果不保存上一個程序的狀態(也就是我們常說的context--上下文),直接切換下一個程序,就會丟失上一個程序的一系列狀態,于是引入了進程這個概念,用以劃分好程序運行時所需要的資源。因此進程就是一個程序運行時候的所需要的基本資源單位(也可以說是程序運行的一個實體)。(系統進行資源分配和調度的基本單位) -
線程
cpu切換多個進程的時候,會花費不少的時間,因為切換進程需要切換到內核態,而每次調度需要內核態都需要讀取用戶態的數據,進程一旦多起來,cpu調度會消耗一大堆資源,因此引入了線程的概念,線程本身幾乎不占有資源,他們共享進程里的資源,內核調度起來不會那么像進程切換那么耗費資源。 -
協程
協程擁有自己的寄存器上下文和棧。協程調度切換時,將寄存器上下文和棧保存到其他地方,在切回來的時候,恢復先前保存的寄存器上下文和棧。因此,協程能保留上一次調用時的狀態(即所有局部狀態的一個特定組合),每次過程重入時,就相當于進入上一次調用的狀態,換種說法:進入上一次離開時所處邏輯流的位置。線程和進程的操作是由程序觸發系統接口,最后的執行者是系統;協程的操作執行者則是用戶自身程序,goroutine也是協程。協程位于線程級別
調度模型簡介
groutine能擁有強大的并發實現是通過GPM調度模型實現。
Go的調度器內部有四個重要的結構:M,P,G,Sched
- M :代表內核級線程,一個M就是一個線程,goroutine就是跑在M之上的;M是一個很大的結構,里面維護小對象內存cache(mcache)、當前執行的goroutine、隨機數發生器等等非常多的信息。
- G :代表一個goroutine,它有自己的棧,instruction pointer和其他信息(正在等待的channel等等),用于調度。
- P :全稱是Processor,處理器,它的主要用途就是用來執行goroutine的,所以它也維護了一個goroutine隊列,里面存儲了所有需要它來執行的goroutine。
- Sched :代表調度器,它維護有存儲M和G的隊列以及調度器的一些狀態信息等。
調度實現場景
一般調度如下圖所示:
從上圖中看,有2個物理線程M1和M2,每一個M都擁有一個處理器P,每一個也都有一個正在運行的goroutine。
P的數量可以通過GOMAXPROCS()來設置,它其實也就代表了真正的并發度,即有多少個goroutine可以同時運行。
圖中灰色的那些goroutine并沒有運行,而是出于ready的就緒態,正在等待被調度。P維護著這個隊列(稱之為runqueue),
Go語言里,啟動一個goroutine很容易:go function 就行,所以每有一個go語句被執行,runqueue隊列就在其末尾加入一個
goroutine,在下一個調度點,就從runqueue中取出(如何決定取哪個goroutine?)一個goroutine執行。
當一個OS線程M1陷入阻塞時:
P轉而在運行M2,圖中的M2可能是正被創建,或者從線程緩存中取出。當M1返回時,它必須嘗試取得一個P來運行goroutine,一般情況下,它會從其他的OS線程那里拿一個P過來,
如果沒有拿到的話,它就把goroutine放在一個global runqueue里,然后自己睡眠(放入線程緩存里)。所有的P也會周期性的檢查global runqueue并運行其中的goroutine,否則global runqueue上的goroutine永遠無法執行。
調度分配不均:如下圖所示
P所分配的任務G很快就執行完了(分配不均),這就導致了這個處理器P很忙,但是其他的P還有任務,此時如果global runqueue沒有任務G了,那么P不得不從其他的P里拿一些G來執行。一般來說,如果P從其他的P那里要拿任務的話,一般就拿run queue的一半 ,這就確保了每個OS線程都能充分的使用。
使用goroutine
goroutine異常捕獲
package main
import (
"fmt"
"time"
)
func addele(a []int ,i int) {
defer func() { //匿名函數捕獲錯誤
err := recover()
if err != nil {
fmt.Println(err)
}
}()
a[i]=i
fmt.Println(a)
}
func main() {
Arry := make([]int,4)
for i :=0 ; i<10 ;i++{
go addele(Arry,i)
}
time.Sleep(time.Second * 2)
}
同步的goroutine
由于goroutine是異步執行的,那很有可能出現主程序退出時還有goroutine沒有執行完,此時goroutine也會跟著退出。此時如果想等到所有goroutine任務執行完畢才退出,go提供了sync包和channel來解決同步問題,當然如果你能預測每個goroutine執行的時間,你還可以通過time.Sleep方式等待所有的groutine執行完成以后在退出程序(如上面的列子)。
示例一:使用sync包同步goroutine
sync大致實現方式:
WaitGroup 等待一組goroutinue執行完畢. 主程序調用 Add 添加等待的goroutinue數量. 每個goroutinue在執行結束時調用 Done ,此時等待隊列數量減1.,主程序通過Wait阻塞,直到等待隊列為0.
package main
import (
"fmt"
"sync"
)
func cal(a int , b int ,n *sync.WaitGroup) {
c := a+b
fmt.Printf("%d + %d = %d\n",a,b,c)
defer n.Done() //goroutinue完成后, WaitGroup的計數-1
}
func main() {
var go_sync sync.WaitGroup //聲明一個WaitGroup變量
for i :=0 ; i<10 ;i++{
go_sync.Add(1) // WaitGroup的計數加1
go cal(i,i+1,&go_sync)
}
go_sync.Wait() //等待所有goroutine執行完畢
}
示例二:通過channel實現goroutine之間的同步。
channel實現方式:
通過channel能在多個groutine之間通訊,當一個goroutine完成時候向channel發送退出信號,等所有goroutine退出時候,利用for循環channel,取channel中的信號,若取不到數據便會阻塞原理,等待所有goroutine執行完畢,使用該方法有個前提是你已經知道了你啟動了多少個goroutine。
package main
import (
"fmt"
"time"
)
func cal(a int , b int ,Exitchan chan bool) {
c := a+b
fmt.Printf("%d + %d = %d\n",a,b,c)
time.Sleep(time.Second*2)
Exitchan <- true
}
func main() {
Exitchan := make(chan bool,10) //聲明并分配管道內存
for i :=0 ; i<10 ;i++{
go cal(i,i+1,Exitchan)
}
for j :=0; j<10; j++{
<- Exitchan //取信號數據,如果取不到則會阻塞
}
close(Exitchan) // 關閉管道
}
goroutine之間的通訊
goroutine本質上是協程,可以理解為不受內核調度,而受go調度器管理的線程。goroutine之間可以通過channel進行通信或者說是數據共享,當然你也可以使用全局變量來進行數據共享。
示例代碼:采用生產者和消費者模式
package main
import (
"fmt"
"sync"
)
func Productor(mychan chan int,data int,wait *sync.WaitGroup) {
mychan <- data
fmt.Println("product data:",data)
defer wait.Done()
}
func Consumer(mychan chan int,wait *sync.WaitGroup) {
a := <- mychan
fmt.Println("consumer data:",a)
defer wait.Done()
}
func main() {
datachan := make(chan int, 100) //通訊數據管道
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
go Productor(datachan, i,&wg) //生產數據
wg.Add(1)
}
for j := 0; j < 10; j++ {
go Consumer(datachan,&wg) //消費數據
wg.Add(1)
}
wg.Wait()
}