1.并發與并行
并發:同一
時間段
執行多個任務
并行:同一
時間點
執行多個任務
- Go語言中的并發通過
實作。goroutine
-
屬于使用者态的線程(協程),支援千萬級的并發。goroutine
-
是由Go語言的goroutine
排程完成的,線程是由作業系統排程完成的。timerun
- Go語言中使用
在多個channel
間進行通訊。goroutine
-
與goroutine
是Go語言秉承了CSP并發模式的基礎實作的。channel
2.goroutine
2.1 使用goroutine
在Go語言中使用goroutine非常簡單,隻需要在調用函數的時候在前面加上
go
關鍵字,就可以為一個函數建立有個
goroutine
。
一個
goroutine
必須對應一個函數,可以建立多個
goroutine
去執行相同的函數。
2.2 啟動單個 goroutine
goroutine
啟動
goroutine
隻需要在調用的函數(匿名函數或者普通函數)前面加一個關鍵字
go
舉個栗子:
package main
import (
"fmt"
"time")
func nowTime() {
nowtime := time.Now()
fmt.Println(nowtime.Format("2006-01-02 15:04:06"))
}
func main() {
go nowTime()
fmt.Println("this is main function")
// time.Sleep(10)
}
當我們不執行
time.Sleep()
時,結果隻列印了
this is main function
這是為什麼?
- 在程式啟動時,Go語言會為
建立一個預設的main()
。goroutine
- 當
函數(主函數)執行完畢的時候該主函數的main
也就結束了。這時建立的分支函數,可能還沒有執行完畢也被強制結束了。goroutine
- 建立新的
需要一些時間的。goroutine
- 是以加上一個
就可以等待分支函數執行完畢。time.Sleep()
2.3 啟動多個 goroutine
goroutine
啟用多個
goroutine
舉個栗子:
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup
func f1(n int64) {
fmt.Println(n)
defer wg.Done()
}
func main() {
for i := 0; i < 10; i++ {
wg.Add(1)
go f1(int64(i))
}
fmt.Println("main")
wg.Wait()
}
2.4 sync.WaitGroup
第一個栗子我們使用的
time.Sleep()
用來阻塞主函數,第二個栗子使用的是
sync.WaitGroup
,二者由什麼差別呢?
使用time.Sleep()将這個阻塞給寫死了,我們不知道子函數執行需要多少時間,然後定死了一個時間。這個有很大的弊端,阻塞時間給多個浪費工作效率,阻塞時間給少了可能子函數還沒有執行完成。
Go語言提供了一個解決方案,
sync.WaitGroup
是一個結構體,在他的内部維護了一個計數器,使用
.Add(1)
時給計數器加1,使用
.Done()
給計數器減1。最後使用
.Wait()
來判斷計數器是否歸零。
方法名 | 功能 |
---|---|
(wg * WaitGroup) Add(delta int) | 計數器+delta |
(wg *WaitGroup) Done() | 計數器-1 |
(wg *WaitGroup) Wait() | 阻塞直到計數器變為0 |
3. goroutine
與線程
goroutine
3.1 可增長的棧
作業系統一般都有固定的棧記憶體,通常為2MB,一個
goroutine
的棧在其生命周期開始時隻有很小的棧(2KB)。
goroutine
的棧不是固定的,可以增大也可以縮小。最大限制為1GB。
在Go語言中一次建立十萬左右的
goroutine
是可以的。
3.2 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排程方面的性能。
3.3 GOMAXPROCS
1.Go運作時的排程器使用GOMAXPROCS參數來确定需要使用多少個OS線程來同時執行Go代碼。
2.預設值是機器上的CPU核心數。例如在一個8核心的機器上,排程器會把Go代碼同時排程到8個OS線程上(GOMAXPROCS是m:n排程中的n)。
3.Go語言中可以通過runtime.GOMAXPROCS()函數設定目前程式并發時占用的CPU邏輯核心數。
4.Go1.5版本之前,預設使用的是單核心執行。
5.Go1.5版本之後,預設使用全部的CPU邏輯核心數。
我們可以通過将任務配置設定到不同的CPU邏輯核心上實作并行的效果
兩個任務隻有一個邏輯核心,此時是做完一個任務再做另一個任務。
舉個栗子
package main
import (
"fmt"
"runtime"
"sync"
)
var wg sync.WaitGroup
func f1() {
defer wg.Done()
for i := 0; i < 10; i++ {
fmt.Println("f1", i)
}
}
func f2() {
defer wg.Done()
for i := 0; i < 10; i++ {
fmt.Println("f2", i)
}
}
func main() {
wg.Add(2)
runtime.GOMAXPROCS(1)
go f1()
go f2()
fmt.Println("main")
wg.Wait()
}
`
main
f2 0
f2 1
f2 2
f2 3
f2 4
f2 5
f2 6
f2 7
f2 8
f2 9
f1 0
f1 1
f1 2
f1 3
f1 4
f1 5
f1 6
f1 7
f1 8
f1 9
`
将邏輯核心數設為2,此時兩個任務并行執行,代碼如下。
package main
import (
"fmt"
"runtime"
"sync"
)
var wg sync.WaitGroup
func f1() {
defer wg.Done()
for i := 0; i < 10; i++ {
fmt.Println("f1", i)
}
}
func f2() {
defer wg.Done()
for i := 0; i < 10; i++ {
fmt.Println("f2", i)
}
}
func main() {
wg.Add(2)
runtime.GOMAXPROCS(12)
go f1()
go f2()
fmt.Println("main")
wg.Wait()
}
1.一個作業系統線程對應多個使用者态
goroutine
2.go程式可以同時使用多個作業系統線程
3.作業系統線程與
goroutine
是多對多關系,即
m:n