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,並且把機器核數也跑滿了,最後連業務程序也跑不了了。