目錄
併發編程前言
進程和線程
A. 進程是程序在操作系統中的一次執行過程,系統進行資源分配和調度的一個獨立單位。
B. 線程是進程的一個執行實體,是CPU調度和分派的基本單位,它是比進程更小的能獨立運行的基本單位。
C.一個進程可以創建和撤銷多個線程;同一個進程中的多個線程之間可以併發執行。
併發和並行
A. 多線程程序在一個核的cpu上運行,就是併發。
B. 多線程程序在多個核的cpu上運行,就是並行。
協程和線程
- 協程:獨立的棧空間,共享堆空間,調度由用戶自己控制,本質上有點類似於用戶級線程,這些用戶級線程的調度也是自己實現的。
- 線程:一個線程上可以跑多個協程,協程是輕量級的線程。
- goroutine 只是由官方實現的超級"線程池"。
- 每個實力
4~5KB
的棧內存佔用和由於實現機制而大幅減少的創建和銷燬開銷是go高併發的根本原因。 - 併發不是並行:
- 併發主要由切換時間片來實現"同時"運行,並行則是直接利用多核實現多線程的運行,go可以設置使用核數,以發揮多核計算機的能力。
- goroutine 奉行通過通信來共享內存,而不是共享內存來通信。
Goroutine
Go語言中的goroutine就是這樣一種機制,goroutine的概念類似於線程,但 goroutine是由Go的運行時(runtime)調度和管理的。Go程序會智能地將 goroutine 中的任務合理地分配給每個CPU。Go語言之所以被稱爲現代化的編程語言,就是因爲它在語言層面已經內置了調度和上下文切換的機制。
在Go語言編程中你不需要去自己寫進程、線程、協程,你的技能包裏只有一個技能–goroutine,當你需要讓某個任務併發執行的時候,你只需要把這個任務包裝成一個函數,開啓一個goroutine去執行這個函數就可以了,就是這麼簡單粗暴。
Go程序會爲main()函數創建一個默認的goroutine
使用goroutine
Go語言中使用goroutine非常簡單,只需要在調用函數的時候在前面加上go關鍵字,就可以爲一個函數創建一個goroutine。
一個goroutine必定對應一個函數,可以創建多個goroutine去執行相同的函數。
啓動多個goroutine
在Go語言中實現併發就是這樣簡單,我們還可以啓動多個goroutine。讓我們再來一個例子: (這裏使用了sync.WaitGroup來實現goroutine的同步)
var wg sync.WaitGroup
func hello(i int) {
defer wg.Done() // goroutine結束就登記-1
fmt.Println("Hello Goroutine!", i)
}
func main() {
for i := 0; i < 10; i++ {
wg.Add(1) // 啓動一個goroutine就登記+1
go hello(i)
}
wg.Wait() // 等待所有登記的goroutine都結束
}
多次執行上面的代碼,會發現每次打印的數字的順序都不一致。這是因爲10個goroutine是併發執行的,而goroutine的調度是隨機的。
注意
- 如果主協程退出了,其他任務還執行嗎(運行下面的代碼測試一下吧)
package main
import (
"fmt"
"time"
)
func main() {
// 合起來寫
go func() {
i := 0
for {
i++
fmt.Printf("new goroutine: i = %d\n", i)
time.Sleep(time.Second)
}
}()
i := 0
for {
i++
fmt.Printf("main goroutine: i = %d\n", i)
time.Sleep(time.Second)
if i == 2 {
break
}
}
}
// 返回結果
//main goroutine: i = 1
//new goroutine: i = 1
//new goroutine: i = 2
//main goroutine: i = 2
goroutine與線程
可增長的棧
OS線程(操作系統線程)一般都有固定的棧內存(通常爲2MB),一個goroutine的棧在其生命週期開始時只有很小的棧(典型情況下2KB),goroutine的棧不是固定的,他可以按需增大和縮小,goroutine的棧大小限制可以達到1GB,雖然極少會用到這個大。所以在Go語言中一次創建十萬左右的goroutine也是可以的。
goroutine調度
GPM是Go語言運行時(runtime)層面的實現,是go語言自己實現的一套調度系統。區別於操作系統調度OS線程。
- 1.G很好理解,就是個goroutine的,裏面除了存放本goroutine信息外 還有與所在P的綁定等信息。
- 2.P管理着一組goroutine隊列,P裏面會存儲當前goroutine運行的上下文環境(函數指針,堆棧地址及地址邊界),P會對自己管理的goroutine隊列做一些調度(比如把佔用CPU時間較長的goroutine暫停、運行後續的goroutine等等)當自己的隊列消費完了就去全局隊列裏取,如果全局隊列裏也消費完了會去其他P的隊列裏搶任務。
- 3.M(machine)是Go運行時(runtime)對操作系統內核線程的虛擬, M與內核線程一般是一一映射的關係, 一個groutine最終是要放到M上執行的;
P與M一般也是一一對應的。他們關係是: P管理着一組G掛載在M上運行。當一個G長久阻塞在一個M上時,runtime會新建一個M,阻塞G所在的P會把其他的G 掛載在新建的M上。當舊的G阻塞完成或者認爲其已經死掉時 回收舊的M。
P的個數是通過runtime.GOMAXPROCS設定(最大256),Go1.5版本之後默認爲物理線程數。 在併發量大的時候會增加一些P和M,但不會太多,切換太頻繁的話得不償失。
單從線程調度講,Go語言相比起其他語言的優勢在於OS線程是由OS內核來調度的,goroutine則是由Go運行時(runtime)自己的調度器調度的,這個調度器使用一個稱爲m:n調度的技術(複用/調度m個goroutine到n個OS線程)。 其一大特點是goroutine的調度是在用戶態下完成的, 不涉及內核態與用戶態之間的頻繁切換,包括內存的分配與釋放,都是在用戶態維護着一塊大的內存池, 不直接調用系統的malloc函數(除非內存池需要改變),成本比調度OS線程低很多。 另一方面充分利用了多核的硬件資源,近似的把若干goroutine均分在物理線程上, 再加上本身goroutine的超輕量,以上種種保證了go調度方面的性能。
runtime包
runtime.Gosched()
讓出CPU時間片,重新等待安排任務
package main
import (
"fmt"
"runtime"
)
func main() {
go func(s string) {
for i := 0; i < 2; i++ {
fmt.Println(s)
}
}("world")
// 主協程
for i := 0; i < 2; i++ {
// 切一下,再次分配任務
runtime.Gosched()
fmt.Println("hello")
}
}
// 返回結果
//world
//world
//hello
//hello
runtime.Goexit()
退出當前協程
package main
import (
"fmt"
"runtime"
)
func main() {
go func() {
defer fmt.Println("A.defer")
func() {
defer fmt.Println("B.defer")
// 結束協程
runtime.Goexit()
defer fmt.Println("C.defer")
fmt.Println("B")
}()
fmt.Println("A")
}()
select {}
}
// 返回結果
//B.defer
//A.defer
//fatal error: all goroutines are asleep - deadlock!
//
//goroutine 1 [select (no cases)]:
//main.main()
runtime.GOMAXPROCS
- Go運行時的調度器使用GOMAXPROCS參數來確定需要使用多少個OS線程來同時執行Go代碼。默認值是機器上的CPU核心數。例如在一個8核心的機器上,調度器會把Go代碼同時調度到8個OS線程上(GOMAXPROCS是m:n調度中的n)。
- Go語言中可以通過runtime.GOMAXPROCS()函數設置當前程序併發時佔用的CPU邏輯核心數。
- Go1.5版本之前,默認使用的是單核心執行。Go1.5版本之後,默認使用全部的CPU邏輯核心數。
我們可以通過將任務分配到不同的CPU邏輯核心上實現並行的效果,這裏舉個例子:
package main
import (
"fmt"
"runtime"
"time"
)
func a() {
for i := 1; i < 10; i++ {
fmt.Println("A:", i)
}
}
func b() {
for i := 1; i < 10; i++ {
fmt.Println("B:", i)
}
}
func main() {
runtime.GOMAXPROCS(1)
go a()
go b()
time.Sleep(time.Second)
}
// 返回結果
//A: 1
//A: 2
//A: 3
//A: 4
//A: 5
//A: 6
//A: 7
//A: 8
//A: 9
//B: 1
//B: 2
//B: 3
//B: 4
//B: 5
//B: 6
//B: 7
//B: 8
//B: 9
兩個任務只有一個邏輯核心,此時是做完一個任務再做另一個任務。 將邏輯核心數設爲2,此時兩個任務並行執行,代碼如下。
package main
import (
"fmt"
"runtime"
"time"
)
func a() {
for i := 1; i < 10; i++ {
fmt.Println("A:", i)
}
}
func b() {
for i := 1; i < 10; i++ {
fmt.Println("B:", i)
}
}
func main() {
runtime.GOMAXPROCS(2)
go a()
go b()
time.Sleep(time.Second)
}
// 返回結果
//B: 1
//B: 2
//B: 3
//B: 4
//B: 5
//B: 6
//B: 7
//B: 8
//B: 9
//A: 1
//A: 2
//A: 3
//A: 4
//A: 5
//A: 6
//A: 7
//A: 8
//A: 9
//
//Process finished with exit code 0
Go語言中的操作系統線程和goroutine的關係:
1.一個操作系統線程對應用戶態多個goroutine。
2.go程序可以同時使用多個操作系統線程。
3.goroutine和OS線程是多對多的關係,即m:n。