Go語言基礎之11--Goroutine

1、建立goroutine

1)在go語言中,每個併發的執行單元叫作一個goroutine;nginx

2)當一個程序啓動時,其主函數即在一個單獨的goroutine中運行,通常這個goroutine是主goroutine;若是想要建立新的goroutine,只須要再執行普通函數或者方法的的前面加上關鍵字go。golang

3)go是如何實現一個多線程程序?小程序

go語言中很簡單,加一個go關鍵字便可,就變成了一個多線程程序(一個進程跑多個線程)網絡

下面經過一個完整實例來了解一下goroutine:多線程

實例1-1         實例1併發

1)未加go關鍵字;異步

package main import ( "fmt" ) func hello() { fmt.Printf("hello\n") } func main() { hello() fmt.Printf("main exited") }

 執行結果以下圖所示:tcp

解釋:函數

能夠發現hello函數和main函數是同一個線程串行執行,先輸出hello,在輸出main exited高併發

 

2) 加上go關鍵字;

package main import ( "fmt" ) func hello() { fmt.Printf("hello\n") } func main() { go hello() fmt.Printf("main exited") }

執行結果以下:

解釋:

加上go關鍵字後,能夠發現hello函數還未執行,程序就退出了。

hello函數加上go關鍵字後,能夠發現其再也不是和main函數同步的執行(串行)了,應該是併發執行了,hello函數至關因而新起了一個線程,和main函數這個線程是獨立的,一共是兩個線程在執行;

在go語言中,以main函數所運行的主線程爲準,若是main函數線程(主線程)執行完畢,還有其餘線程(附屬主線程的子線程)在執行,這些未執行完畢的線程也會被強制關閉。(main函數所在的主線程一旦退出,就至關於整個進程退出,進程一退出,這個進程中的其餘線程也會被強制關閉退出)

針對咱們這個實例:其實就是main函數所在的主線程已經執行完畢了,hello函數所在的線程效率沒有main函數所在的主線程高,因此輸出結果沒有hello函數的內容。

 

3) 加上go關鍵字完整版代碼;

package main import ( "fmt"
    "time" ) func hello() { fmt.Printf("hello\n") } func main() { go hello() time.Sleep(1 * time.Second) //經過睡1秒,保證hello函數所在線程執行
    fmt.Printf("main exited") }

 執行結果以下圖所示:

解釋:

經過sleep 1秒,hello函數所在線程得以執行完畢。

 

實例1-2         實例2

1)不加go關鍵字

package main import ( "fmt"
    "time" ) func hello() { for i := 0; i < 4; i++ { fmt.Printf("hello:%d\n", i) time.Sleep(time.Millisecond * 10) } } func main() { hello() for i := 0; i < 4; i++ { fmt.Printf("main:%d\n", i) time.Sleep(time.Millisecond * 10) } time.Sleep(time.Second) }

 執行結果以下圖所示:

解釋:

能夠看到不加go關鍵字,是串行執行。

 

2)加上go關鍵字

package main import ( "fmt"
    "time" ) func hello() { for i := 0; i < 4; i++ { fmt.Printf("hello:%d\n", i) time.Sleep(time.Millisecond * 10) } } func main() { go hello() for i := 0; i < 4; i++ { fmt.Printf("main:%d\n", i) time.Sleep(time.Millisecond * 10) } time.Sleep(time.Second) }

 執行結果以下圖所示:

解釋:

經過打印結果能夠看到hello函數所在的線程和main函數所在的主線程是並行執行的。

2、建立多個goroutine

 實例以下:

package main import ( "fmt"
    "time" ) func numbers() { for i := 1; i <= 5; i++ { time.Sleep(250 * time.Millisecond)   //每隔250ms打印一個整數
        fmt.Printf("%d ", i) } } func alphabets() { for i := 'a'; i <= 'e'; i++ {   //字符的底層也是一個整數
        time.Sleep(400 * time.Millisecond)   //每隔400ms打印一個字母
        fmt.Printf("%c ", i) } } func main() { go numbers() go alphabets() time.Sleep(3000 * time.Millisecond)    //主線程等待3秒在退出
    fmt.Println("main terminated") }

執行結果以下:

解釋:

根據輸出結果,能夠看到numbers函數、alphabets函數所在的線程是並行執行的。

(補充:numbers函數、alphabets函數都是新起一個線程,至於哪一個線程先啓動,這是不必定的,不是說number函數在前,其的就先啓動,具體看cpu的調度策略的)

3、進程和線程

1)進程是程序在操做系統中的一次執行過程,系統進行資源分配和調度的

一個獨立單位。

2)線程是進程的一個執行實體,是CPU調度和分派的基本單位,它是比進程更

小的能獨立運行的基本單位。

3)一個進程能夠建立和撤銷多個線程;同一個進程中的多個線程之間能夠併發

執行

圖示:

4、併發和並行

1)多線程程序在一個核的cpu上運行,就是併發

2)多線程程序在多個核的cpu上運行,就是並行

圖示:

解釋:

左圖(單核)爲併發,右圖(多核)爲並行。

x軸爲時間軸,左圖併發,同一時間段只能有一個程序在執行,右圖並行,同一時間段內能夠有多個程序執行。

5、協程和線程

協程

獨立的棧空間,共享堆空間,調度由用戶本身控制,本質上有點相似於

用戶級線程,這些用戶級線程的調度也是本身實現的

線程

線程是操做系統起的,由操做系統進行管理;

一個線程上能夠跑多個協程,協程是輕量級的線程。

 

葵花寶典:

操做系統起的線程有一個問題:

操做系統就是內核,操做系統起的線程執行在內核態,用戶的程序應用運行在用戶態,這是兩個不一樣的形態,若是咱們是建立線程的話,線程是操做系統在管理,若是咱們在應用程序中建立線程,線程之間要進行切換的話,就須要進入內核態去進行切換,由於這個線程是內核去控制管理的,若是在一個應用程序中建立了幾千個線程,這樣線程之間的切換每次都要從用戶態到內核態去切換,上下文切換比較頻繁,用戶態到內核態切換是須要消耗資源的,最終的結果就是,上千個線程,上下文切換會形成系統變得很是緩慢,這也是線程的一個缺點,這也是其餘語言的缺點,跑幾千個線程就跑不動了,因此其餘語言是須要搞一個線程池。

可是在go語言的話,go語言已經幫助咱們解決了上述問題,協程是用戶態的一個概念,協程和操做系統的線程是兩回事,一個操做系統的線程可能執行多個協程,協程是go語言幫咱們抽象出來的一個東西,能夠發現,協程之間的切換不用和內核態去作一個相互切換,僅僅只在用戶態進行切換(線程是須要內核來進行控制切換)。用戶態切換要比到內核態進行切換性能要高很是多,因此,go語言中,起上千個協程是徹底沒有問題的,上萬個都沒問題(起上萬個協程的話,對應的可能就是操做系統的幾十個線程)。因此說在go語言中,徹底不用擔憂起上千個線程會影響系統,特別是在高併發領域http服務,基本上都是來一個請求,就會起一個協程的。這樣你同時有幾萬個併發請求過來,就有幾萬個goroutine再跑,這個性能是很是高的。(nginx並非每個請求起一個線程,而是經過異步進行處理,一個線程異步的處理多個請求,這對go來講,寫一個高併發程序太簡單了。http、tcp服務,來一個請求起一個goroutine便可。)

6、goroutine調度模型

圖示1:

解釋:

M是操做系統的線程,P是一個上下文,G是goroutine

 

圖示2:

 

解釋:

1)goroutine必需要有上下文才會執行,左邊藍色的能夠看見已經有一個goroutine在執行了,右邊灰色的是等待被執行的goroutine,一個物理線程能夠執行多個goroutine,一個goroutine執行完,下一個等待的goroutine被執行,這其實就是go語言中的一個線程調度狀況。

2)好比說一個正在執行的goroutine,其須要暫停,這個時候就要作一個上下文切換(語句執行到哪裏等的上下文要保存起來)

3)目前上圖狀況是:一個程序跑了兩個物理線程,一個物理線程跑多個goroutine

 

圖示3:

 

解釋:

M0(物理線程)跑了一個G0(goroutine),同時還有3個goroutine在等待執行,當G0有IO操做或者有網絡操做時,此時要進行一個寫文件操做,寫文件是一個慢操做,磁盤和內存寫數據速度相差不少倍(磁盤寫文件5ms,內存讀數據不到1微妙),此時咱們花5ms去同步等待時,cpu的資源就浪費了,這種狀況的,發現G0是作IO操做的話,就會把G0和M0給剝離出來,而後會新建一個線程M1,接着執行後面的goroutine。

 

注意:

go語言:一個cpu同一時間一個線程只能跑一個goroutine,其牛逼的地方在於減小了用戶態和內核態的切換。

7、設置golang運行的cpu核數

一、主要藉助的是runtime包的NumCPU函數;

二、能夠經過runtime包中的GOMAXPROCS函數來控制使用cpu的個數;

三、新版本默認跑滿全部cpu個數

注意:

在golang1.6版本以前,goroutine程序默認只能跑在1個cpu上,若是要想多核跑的話,就須要利用GOMAXPROCS函數來控制CPU核數,新版本目前是不用設置了,默認是將計算機全部cpu核數跑滿。

實例:

咱們下面經過一個實例來對比一下:

1)限制使用的cpu核數爲1

package main import ( "fmt"
    "runtime"
    "time" ) func main() { cpu := runtime.NumCPU() runtime.GOMAXPROCS(1) //只限制使用cpu的1個核

    for i := 0; i < 8; i++ { //起8個goroutine,每一個goroutine來跑一個無限循環的匿名函數
 go func() { for { } }() } fmt.Printf("%d\n", cpu) time.Sleep(15 * time.Second) }

執行結果:

 

CPU示意圖:

 

咱們能夠發現cpu是沒有跑滿的,達到了預計效果。

 

2)  不限制cpu的使用核數

package main import ( "fmt"
    "runtime"
    "time" ) func main() { cpu := runtime.NumCPU() //不限制CPU使用個數

    for i := 0; i < 8; i++ { //起8個goroutine,每一個goroutine來跑一個無限循環的匿名函數
 go func() { for { } }() } fmt.Printf("%d\n", cpu) time.Sleep(15 * time.Second) }

 執行結果:

 

CPU示意圖:

 

咱們能夠發現CPU已將徹底跑滿了。

 

總結:

咱們往後使用時,若是隻是一個監控、收集日誌小程序,要適當控制一下CPU使用個數,若是不控制的話,程序出現bug,並且把機器核數也跑滿了,最後連業務程序也跑不了了。

相關文章
相關標籤/搜索