goroutine的多核並行化,讓出時間片

村望老弟發表於2021-10-29

1.多核並行

Go 1.5開始, Go的GOMAXPROCS預設值已經設定為 CPU的核數, 這允許我們的Go程式充分使用機器的每一個CPU,最大程度的提高我們程式的併發效能。

runtime.GOMAXPROCS(16)

到底應該設定多少個CPU核心呢,其實runtime包中還提供了另外一個函式NumCPU()來獲
取核心數。可以看到,Go語言其實已經感知到所有的環?資訊,下一版本中完全可以利用這些 資訊將goroutine排程到所有CPU核心上,從而最大化地利用伺服器的多核計算能力。棄 GOMAXPROCS只是個時間問題。

    fmt.Printf("runtime.NumCPU(): %v\n", runtime.NumCPU()) //runtime.NumCPU(): 12

看下面一段簡單的程式碼

package main

import (
    "fmt"
    "sync"
)

var wg sync.WaitGroup

func myPrintA() {
    defer wg.Done()
    fmt.Println("A")
}
func myPrintB() {
    defer wg.Done()
    fmt.Println("B")
}
func main() {
    for i := 1; i <= 10; i++ {
        wg.Add(2)
        go myPrintA()
        go myPrintB()
    }
    wg.Wait()
}

嘗試執行發現A,B的輸出並沒有規律,帶有隨機性
執行結果:

[Running] go run "/Users/codehope/Study/time-to-go/Code/re.channel/cpu_mult_calc.go"
A
B
A
A
B
A
B
A
B
A
B
B
B
A
A
B
A
A
B
B
[Done] exited with code=0 in 0.364 seconds

證明,每次迴圈開啟的兩個go程是並行的,因為在目前我的這個版本,預設 Go的GOMAXPROCS預設值已經設定為 CPU的核數,如果我設定Go的GOMAXPROCS為1,代表這些goroutine都執行在一個cpu核心上,在一個goroutine得到時間片執行的時候,其他goroutine 都會處於等待狀態

package main

import (
    "fmt"
    "runtime"
    "sync"
)

var wg sync.WaitGroup

func myPrintA() {
    defer wg.Done()
    fmt.Println("A")
}
func myPrintB() {
    defer wg.Done()
    fmt.Println("B")
}
func main() {
    runtime.GOMAXPROCS(1)
    for i := 1; i <= 10; i++ {
        wg.Add(2)
        go myPrintA()
        go myPrintB()
    }
    wg.Wait()
}

設定runtime.GOMAXPROCS(1)後,再次執行,不管執行幾次,都是兩個交替輸出,很規律

[Running] go run "/Users/codehope/Study/time-to-go/Code/re.channel/cpu_mult_calc.go"
B
A
B
A
B
A
B
A
B
A
B
A
B
A
B
A
B
A
B
A
[Done] exited with code=0 in 0.353 seconds

因為每次迴圈建立的go程,都在同一個cpu核心上,對應GPM模型的P佇列只有一個,需要排隊執行,所以就出現了上面的輸出結果

2.讓出時間片

我們可以在每個goroutine中控制何時主動出讓時間片給其他goroutine,這可以使用runtime 包中的Gosched()函式實現。
實際上,如果要比較精細地控制goroutine的行為,就必須比較深入地瞭解Go語言開發包中 runtime包所提供的具體功能。

我們依舊是對上面的程式碼進行改造:(同樣是 Go的GOMAXPROCS為1,goroutine p佇列為1,多個go程無法並行的情況下)

package main

import (
    "fmt"
    "runtime"
    "sync"
)

var wg sync.WaitGroup

func myPrintA() {
    defer wg.Done()
    fmt.Println("A")
}
func myPrintB() {
    defer wg.Done()
    runtime.Gosched() //列印B之前,讓出當前goroutine所佔的時間片
    fmt.Println("B")
}
func main() {
    runtime.GOMAXPROCS(1)
    for i := 1; i <= 10; i++ {
        wg.Add(2)
        go myPrintA()
        go myPrintB()
    }
    wg.Wait()
}

看到上面的程式碼,我們在列印B之前,讓出當前goroutine所佔的時間片,這個輸出結果會是什麼呢?

[Running] go run "/Users/codehope/Study/time-to-go/Code/re.channel/tempCodeRunnerFile.go"
A
A
A
A
A
A
A
A
A
A
B
B
B
B
B
B
B
B
B
B
[Done] exited with code=0 in 0.574 seconds

可以看到,先把A全部列印,然後才去列印的B,因為每次迴圈開啟的兩個goroutine,是交替執行,當執行myPrintB的go協程搶到時間片的時候,在內部,執行 fmt.Println("B")之前,將當前goroutine,搶到的時間片讓出,儲存當前的狀態,等再次搶到了時間片,就繼續執行,這裡可能會有點小疑問(那為什麼當前迴圈的B沒有繼續執行呢?而且全部先執行的輸出A呢?)針對這個疑問,先留下一個猜想(當前時間片讓出後,被下一個迴圈的goroutine搶去了,如果當前迴圈的時間足夠的話(不會那麼快進行到下次迴圈,就不會建立新的goroutine),可能就可以在當前迴圈中執行了),下面我們就去證實我們的猜想,我們在每次迴圈中,讓程式sleep 1s

package main

import (
    "fmt"
    "runtime"
    "sync"
    "time"
)

var wg sync.WaitGroup

func myPrintA() {
    defer wg.Done()
    fmt.Println("A")
}
func myPrintB() {
    defer wg.Done()
    runtime.Gosched()
    fmt.Println("B")
}
func main() {
    runtime.GOMAXPROCS(1)
    for i := 1; i <= 10; i++ {
        wg.Add(2)
        go myPrintA()
        go myPrintB()
        time.Sleep(time.Second)
    }
    wg.Wait()
}

輸出結果:隔一秒,輸出一次A,B,(B讓出時間片後,還能再次搶到時間片繼續執行自己下面程式碼,因為當前有足夠的時間和空閒的時間片給他用,不會那麼快被下次迴圈建立的goroutine搶去!)

[Running] go run "/Users/codehope/Study/time-to-go/Code/re.channel/cpu_mult_calc.go"
A
B
A
B
A
B
A
B
A
B
A
B
A
B
A
B
A
B
A
B
[Done] exited with code=0 in 10.569 seconds
本作品採用《CC 協議》,轉載必須註明作者和本文連結
CunWang@Ch

相關文章