Goroutine、操作系統(tǒng)線程和CPU的管理

【譯文】原文地址
本文是基于Go 1.13版本
創(chuàng)建一個(gè)操作系統(tǒng)線程或從一個(gè)線程切換到另一個(gè)線程,對程序來說在內(nèi)存和性能方面是代價(jià)很大的。Go為了充分利用cpu核的優(yōu)勢,在一開始就考慮了并發(fā)性。

M, P, G組合

為了解決性能問題,Go有自己的調(diào)度器來分配goroutine到線程上去。該調(diào)度器定義了三個(gè)主要概念,就如代碼中所表示的:
G-指的是goroutine
M-工作線程,或者機(jī)器
P-處理器,執(zhí)行代碼需要的資源。M必須有相應(yīng)的P才能運(yùn)行代碼。
如下是M, P, G模型圖:


M, P, G模型圖

每個(gè)goroutine(G)運(yùn)行在一個(gè)OS線程上,這個(gè)線程會(huì)被分配到一個(gè)邏輯CPU(P)。通過一個(gè)例子來看下Go如何管理它們的:

package main

import "sync"

func main() {
    var wg sync.WaitGroup
    wg.Add(2)
    go func() {
        println(`hello`)
        wg.Done()
    }()
    go func() {
        println(`world`)
        wg.Done()
    }()
    wg.Wait()
}

Go首先將根據(jù)機(jī)器的邏輯cpu個(gè)數(shù)來創(chuàng)建不同的P,并存放在一個(gè)空閑的P列表中:


P初始化

然后,新的一個(gè)goroutine或多個(gè)goroutine將喚醒P來更好的分配工作。這個(gè)P將創(chuàng)建一個(gè)與操作系統(tǒng)線程相關(guān)的M:


OS Thread的創(chuàng)建

然而,空閑的P和M-即沒有等待執(zhí)行的goroutine,從系統(tǒng)調(diào)用中返回或甚至被垃圾收集器強(qiáng)制停止,將進(jìn)入空閑隊(duì)列中:
M, P空閑隊(duì)列

在程序的啟動(dòng)過程中,Go已經(jīng)創(chuàng)建了一些OS線程和相關(guān)的M。在以上例子中,第一個(gè)輸出hello的goroutine將使用main goroutine,而第二個(gè)goroutine將從空閑列表中獲取一個(gè)M和P:


從空閑列表獲取M和P

以上我們看到了goroutine和線程的管理,下面看下在什么情況下Go會(huì)使用更多的M而不是P以及在系統(tǒng)調(diào)用下goroutine是如何管理的。

系統(tǒng)調(diào)用

Go優(yōu)化系統(tǒng)調(diào)用的方法是通過在運(yùn)行時(shí)對其進(jìn)行包裝,無論系統(tǒng)調(diào)用是否阻塞。這個(gè)包裝器將自動(dòng)將P和線程M分離,并允許其他線程在其上運(yùn)行。讓我們舉一個(gè)讀取文件的例子:

package main

import (
    "os"
)

func main() {
    buf := make([]byte, 0, 2)

    fd, _ := os.Open("main.go")
    fd.Read(buf)
    fd.Close()

    println(string(buf)) // 42
}

下面是文件打開的工作流程:


系統(tǒng)調(diào)用線程交出P

P0進(jìn)入空閑隊(duì)列,可供其他線程使用。一旦系統(tǒng)調(diào)用退出,Go將應(yīng)用如下滿足條件的規(guī)則:

  • 嘗試獲取對應(yīng)的P,本例中的p0,并繼續(xù)執(zhí)行。
  • 嘗試從空閑列表中獲取一個(gè)新的P,恢復(fù)執(zhí)行。
  • 將正在執(zhí)行的goroutine放入全局隊(duì)列,并將對于的M放回空閑隊(duì)列。
    然而,Go也可以處理資源尚未就緒的情況,比如http調(diào)用這種非阻塞I/O情況。在這種情況下,第一個(gè)系統(tǒng)調(diào)用(遵循前面的共工作流程)因?yàn)橘Y源未就緒將調(diào)用不成功,迫使Go使用網(wǎng)絡(luò)輪詢并使goroutine駐留。
func main() {
   http.Get(`https://httpstat.us/200`)
}

一旦第一個(gè)系統(tǒng)調(diào)用完成并顯示資源未就緒,對應(yīng)的goroutine就會(huì)駐留,直到網(wǎng)絡(luò)輪詢通知資源就緒。在這種情況下,線程M將不會(huì)被阻塞:


網(wǎng)絡(luò)輪詢等待資源就緒

當(dāng)Go調(diào)度器尋找工作時(shí),對應(yīng)的goroutine將再次運(yùn)行。然后調(diào)度器將詢問網(wǎng)絡(luò)輪詢器,是否有等待就緒的gorotine需要執(zhí)行。



如果就緒的goroutine不止一個(gè)的話,多出的goroutine將進(jìn)入全局可執(zhí)行隊(duì)列等待調(diào)度。

操作系統(tǒng)線程約束

當(dāng)使用系統(tǒng)調(diào)用時(shí),Go不限制被阻塞的操作系統(tǒng)線程數(shù)量,如下所述:
GOMAXPROCS變量限制了能夠同時(shí)執(zhí)行用戶態(tài)Go代碼的操作系統(tǒng)線程數(shù)量。但并沒有對Go代碼中處于阻塞狀態(tài)下系統(tǒng)調(diào)用的線程數(shù)量有限制。這些系統(tǒng)調(diào)用線程數(shù)量不會(huì)受影響。
以下就是這種情況的一個(gè)例子:

package main

import (
    "fmt"
    "net/http"
    "sync"
)

func main() {
        var wg sync.WaitGroup

        for i := 0;i < 100 ;i++  {
            wg.Add(1)

            go func(i int) {
                http.Get(`https://www.baidu.com`)
                fmt.Println(i)
                wg.Done()
            }(i)
        }

        wg.Wait()
    }

如下是跟蹤工具查看到的線程個(gè)數(shù):


image.png

因?yàn)镚o優(yōu)化了線程的使用,當(dāng)goroutine被阻塞時(shí)線程會(huì)被復(fù)用,這就解釋了為什么這個(gè)線程數(shù)和循環(huán)數(shù)量不一致。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容