goroutine簡(jiǎn)介
goroutine是go語(yǔ)言中最為NB的設(shè)計(jì)芥吟,也是其魅力所在侦铜,goroutine的本質(zhì)是協(xié)程专甩,是實(shí)現(xiàn)并行計(jì)算的核心。goroutine使用方式非常的簡(jiǎn)單钉稍,只需使用go關(guān)鍵字即可啟動(dòng)一個(gè)協(xié)程涤躲,并且它是處于異步方式運(yùn)行,你不需要等它運(yùn)行完成以后在執(zhí)行以后的代碼贡未。GO默認(rèn)是使用一個(gè)CPU核的种樱,除非設(shè)置runtime.GOMAXPROCS。
go func()//通過(guò)go關(guān)鍵字啟動(dòng)一個(gè)協(xié)程來(lái)運(yùn)行函數(shù)
概念普及
-
并發(fā):
一個(gè)cpu上能同時(shí)執(zhí)行多項(xiàng)任務(wù)俊卤,在很短時(shí)間內(nèi)嫩挤,cpu來(lái)回切換任務(wù)執(zhí)行(在某段很短時(shí)間內(nèi)執(zhí)行程序a,然后又迅速得切換到程序b去執(zhí)行)消恍,有時(shí)間上的重疊(宏觀上是同時(shí)的岂昭,微觀仍是順序執(zhí)行),這樣看起來(lái)多個(gè)任務(wù)像是同時(shí)執(zhí)行,這就是并發(fā)哺哼。 -
并行
當(dāng)系統(tǒng)有多個(gè)CPU(多核)時(shí),每個(gè)CPU同一時(shí)刻都運(yùn)行任務(wù)佩抹,互不搶占自己所在的CPU資源,同時(shí)進(jìn)行取董,稱為并行棍苹。 -
進(jìn)程
cpu在切換程序的時(shí)候,如果不保存上一個(gè)程序的狀態(tài)(也就是我們常說(shuō)的context--上下文)茵汰,直接切換下一個(gè)程序枢里,就會(huì)丟失上一個(gè)程序的一系列狀態(tài),于是引入了進(jìn)程這個(gè)概念蹂午,用以劃分好程序運(yùn)行時(shí)所需要的資源栏豺。因此進(jìn)程就是一個(gè)程序運(yùn)行時(shí)候的所需要的基本資源單位(也可以說(shuō)是程序運(yùn)行的一個(gè)實(shí)體)。(系統(tǒng)進(jìn)行資源分配和調(diào)度的基本單位) -
線程
cpu切換多個(gè)進(jìn)程的時(shí)候豆胸,會(huì)花費(fèi)不少的時(shí)間奥洼,因?yàn)榍袚Q進(jìn)程需要切換到內(nèi)核態(tài),而每次調(diào)度需要內(nèi)核態(tài)都需要讀取用戶態(tài)的數(shù)據(jù)晚胡,進(jìn)程一旦多起來(lái)灵奖,cpu調(diào)度會(huì)消耗一大堆資源,因此引入了線程的概念估盘,線程本身幾乎不占有資源瓷患,他們共享進(jìn)程里的資源,內(nèi)核調(diào)度起來(lái)不會(huì)那么像進(jìn)程切換那么耗費(fèi)資源遣妥。 -
協(xié)程
協(xié)程擁有自己的寄存器上下文和棧擅编。協(xié)程調(diào)度切換時(shí),將寄存器上下文和棧保存到其他地方,在切回來(lái)的時(shí)候爱态,恢復(fù)先前保存的寄存器上下文和棧谭贪。因此,協(xié)程能保留上一次調(diào)用時(shí)的狀態(tài)(即所有局部狀態(tài)的一個(gè)特定組合)肢藐,每次過(guò)程重入時(shí)故河,就相當(dāng)于進(jìn)入上一次調(diào)用的狀態(tài),換種說(shuō)法:進(jìn)入上一次離開時(shí)所處邏輯流的位置吆豹。線程和進(jìn)程的操作是由程序觸發(fā)系統(tǒng)接口鱼的,最后的執(zhí)行者是系統(tǒng);協(xié)程的操作執(zhí)行者則是用戶自身程序痘煤,goroutine也是協(xié)程凑阶。協(xié)程位于線程級(jí)別
調(diào)度模型簡(jiǎn)介
groutine能擁有強(qiáng)大的并發(fā)實(shí)現(xiàn)是通過(guò)GPM調(diào)度模型實(shí)現(xiàn)。
Go的調(diào)度器內(nèi)部有四個(gè)重要的結(jié)構(gòu):M衷快,P宙橱,G,Sched
- M :代表內(nèi)核級(jí)線程蘸拔,一個(gè)M就是一個(gè)線程师郑,goroutine就是跑在M之上的;M是一個(gè)很大的結(jié)構(gòu)调窍,里面維護(hù)小對(duì)象內(nèi)存cache(mcache)宝冕、當(dāng)前執(zhí)行的goroutine、隨機(jī)數(shù)發(fā)生器等等非常多的信息邓萨。
- G :代表一個(gè)goroutine地梨,它有自己的棧,instruction pointer和其他信息(正在等待的channel等等)缔恳,用于調(diào)度宝剖。
- P :全稱是Processor,處理器歉甚,它的主要用途就是用來(lái)執(zhí)行g(shù)oroutine的万细,所以它也維護(hù)了一個(gè)goroutine隊(duì)列,里面存儲(chǔ)了所有需要它來(lái)執(zhí)行的goroutine纸泄。
- Sched :代表調(diào)度器赖钞,它維護(hù)有存儲(chǔ)M和G的隊(duì)列以及調(diào)度器的一些狀態(tài)信息等。
調(diào)度實(shí)現(xiàn)場(chǎng)景
一般調(diào)度如下圖所示:
從上圖中看刃滓,有2個(gè)物理線程M1和M2,每一個(gè)M都擁有一個(gè)處理器P耸弄,每一個(gè)也都有一個(gè)正在運(yùn)行的goroutine咧虎。
P的數(shù)量可以通過(guò)GOMAXPROCS()來(lái)設(shè)置,它其實(shí)也就代表了真正的并發(fā)度计呈,即有多少個(gè)goroutine可以同時(shí)運(yùn)行砰诵。
圖中灰色的那些goroutine并沒有運(yùn)行征唬,而是出于ready的就緒態(tài),正在等待被調(diào)度茁彭。P維護(hù)著這個(gè)隊(duì)列(稱之為runqueue)总寒,
Go語(yǔ)言里,啟動(dòng)一個(gè)goroutine很容易:go function 就行理肺,所以每有一個(gè)go語(yǔ)句被執(zhí)行摄闸,runqueue隊(duì)列就在其末尾加入一個(gè)
goroutine,在下一個(gè)調(diào)度點(diǎn)妹萨,就從runqueue中取出(如何決定取哪個(gè)goroutine年枕?)一個(gè)goroutine執(zhí)行。
當(dāng)一個(gè)OS線程M1陷入阻塞時(shí):
P轉(zhuǎn)而在運(yùn)行M2乎完,圖中的M2可能是正被創(chuàng)建熏兄,或者從線程緩存中取出。當(dāng)M1返回時(shí)树姨,它必須嘗試取得一個(gè)P來(lái)運(yùn)行g(shù)oroutine摩桶,一般情況下,它會(huì)從其他的OS線程那里拿一個(gè)P過(guò)來(lái)帽揪,
如果沒有拿到的話硝清,它就把goroutine放在一個(gè)global runqueue里,然后自己睡眠(放入線程緩存里)台丛。所有的P也會(huì)周期性的檢查global runqueue并運(yùn)行其中的goroutine耍缴,否則global runqueue上的goroutine永遠(yuǎn)無(wú)法執(zhí)行。
調(diào)度分配不均:如下圖所示
P所分配的任務(wù)G很快就執(zhí)行完了(分配不均)挽霉,這就導(dǎo)致了這個(gè)處理器P很忙防嗡,但是其他的P還有任務(wù),此時(shí)如果global runqueue沒有任務(wù)G了侠坎,那么P不得不從其他的P里拿一些G來(lái)執(zhí)行蚁趁。一般來(lái)說(shuō),如果P從其他的P那里要拿任務(wù)的話实胸,一般就拿run queue的一半 他嫡,這就確保了每個(gè)OS線程都能充分的使用。
使用goroutine
goroutine異常捕獲
package main
import (
"fmt"
"time"
)
func addele(a []int ,i int) {
defer func() { //匿名函數(shù)捕獲錯(cuò)誤
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是異步執(zhí)行的庐完,那很有可能出現(xiàn)主程序退出時(shí)還有g(shù)oroutine沒有執(zhí)行完钢属,此時(shí)goroutine也會(huì)跟著退出。此時(shí)如果想等到所有g(shù)oroutine任務(wù)執(zhí)行完畢才退出门躯,go提供了sync包和channel來(lái)解決同步問(wèn)題淆党,當(dāng)然如果你能預(yù)測(cè)每個(gè)goroutine執(zhí)行的時(shí)間,你還可以通過(guò)time.Sleep方式等待所有的groutine執(zhí)行完成以后在退出程序(如上面的列子)。
示例一:使用sync包同步goroutine
sync大致實(shí)現(xiàn)方式:
WaitGroup 等待一組goroutinue執(zhí)行完畢. 主程序調(diào)用 Add 添加等待的goroutinue數(shù)量. 每個(gè)goroutinue在執(zhí)行結(jié)束時(shí)調(diào)用 Done 染乌,此時(shí)等待隊(duì)列數(shù)量減1.山孔,主程序通過(guò)Wait阻塞,直到等待隊(duì)列為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的計(jì)數(shù)-1
}
func main() {
var go_sync sync.WaitGroup //聲明一個(gè)WaitGroup變量
for i :=0 ; i<10 ;i++{
go_sync.Add(1) // WaitGroup的計(jì)數(shù)加1
go cal(i,i+1,&go_sync)
}
go_sync.Wait() //等待所有g(shù)oroutine執(zhí)行完畢
}
示例二:通過(guò)channel實(shí)現(xiàn)goroutine之間的同步荷憋。
channel實(shí)現(xiàn)方式:
通過(guò)channel能在多個(gè)groutine之間通訊台颠,當(dāng)一個(gè)goroutine完成時(shí)候向channel發(fā)送退出信號(hào),等所有g(shù)oroutine退出時(shí)候,利用for循環(huán)channel,取channel中的信號(hào)勒庄,若取不到數(shù)據(jù)便會(huì)阻塞原理串前,等待所有g(shù)oroutine執(zhí)行完畢,使用該方法有個(gè)前提是你已經(jīng)知道了你啟動(dòng)了多少個(gè)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) //聲明并分配管道內(nèi)存
for i :=0 ; i<10 ;i++{
go cal(i,i+1,Exitchan)
}
for j :=0; j<10; j++{
<- Exitchan //取信號(hào)數(shù)據(jù)酪呻,如果取不到則會(huì)阻塞
}
close(Exitchan) // 關(guān)閉管道
}
goroutine之間的通訊
goroutine本質(zhì)上是協(xié)程,可以理解為不受內(nèi)核調(diào)度盐须,而受go調(diào)度器管理的線程玩荠。goroutine之間可以通過(guò)channel進(jìn)行通信或者說(shuō)是數(shù)據(jù)共享,當(dāng)然你也可以使用全局變量來(lái)進(jìn)行數(shù)據(jù)共享贼邓。
示例代碼:采用生產(chǎn)者和消費(fèi)者模式
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) //通訊數(shù)據(jù)管道
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
go Productor(datachan, i,&wg) //生產(chǎn)數(shù)據(jù)
wg.Add(1)
}
for j := 0; j < 10; j++ {
go Consumer(datachan,&wg) //消費(fèi)數(shù)據(jù)
wg.Add(1)
}
wg.Wait()
}