Go 面試官:進程、線程都有 ID,爲何 Goroutine 沒有 ID?

微信搜索【 腦子進煎魚了】關注這一隻爆肝煎魚。本文 GitHub eddycjy/blog 已收錄,有個人系列文章、資料和開源 Go 圖書。

你們好,我是煎魚。git

最近金三銀四,是面試的季節。在個人 Go 讀者交流羣裏出現了許多小夥伴在討論本身面試過程當中所遇到的一些 Go 面試題。github

今天的主角,是你們在既有語言基礎的狀況下,學 Goroutine 時會容易糾結的一點。就是 「進程、線程都有 ID,爲何 Goroutine 沒有 GoroutineID?」。golang

這是爲何呢,怎麼作那些跨協程處理呢?面試

GoroutineID 是什麼

咱們要知道,爲何你們會下意識的想去要 GoroutineID,下面引用 Go 語言聖經中的表述:算法

在大多數支持多線程的操做系統和程序語言中,當前的線程都有一個獨特的身份(ID),而且這個身份信息能夠以一個普通值的形式被很容易地獲取到,典型的能夠是一個 integer 或者指針值。這種狀況下咱們作一個抽象化的 thread-local storage(線程本地存儲,多線程編程中不但願其它線程訪問的內容)就很容易,只須要以線程的 ID 做爲 key 的一個 map 就能夠解決問題,每個線程以其 ID 就能從中獲取到值,且和其它線程互不衝突。

也就在常規的進程、線程中都有其 ID 的概念,咱們能夠在程序中經過 ID 來獲取其餘進程、線程中的數據,甚至是傳輸數據。就像一把鑰匙同樣,有了他幹啥均可以。編程

GoroutineID 的概念也是相似的,也就是協程的 ID。咱們下意識的就指望經過協程 ID 來進行跨協程的操做。微信

但,在 Go 語言中 GoroutineID 並無顯式獲取的辦法,這就要打個大大的疑惑了。多線程

爲何沒有 GoroutineID

爲何在 Go 語言中沒有 GoroutineID 呢,是從一開始就沒有的,仍是,這樣子設計的緣由是什麼呢?性能

其實 Go 語言在之前是有暴露方法去獲取 GoroutineID 的,但在 Go1.4 後就把該方法給隱藏起來了,不建議你們使用。ui

也就是明面上沒有 GoroutineID,是一個有意而爲之的行爲。緣由是:根據以往的經驗,認爲 thread-local storage 存在被濫用的可能性,且帶來許多沒必要要的複雜度

簡單來說,Andrew Gerrand 的回答是 」thread-local storage 的成本遠遠超過了它們的收益。它們只是不適合 Go 語言。」

潛在的問題

  • 當 Goroutine 消失時:

    • 它的 Goroutine 本地存儲將不會被 GC 化。 (你能夠獲得 goid 的當前的 Goroutine,但你不能獲得全部運行的 Goroutine 的列表)
  • 若是處理程序本身產生了新的 Goroutine 怎麼辦?

    • 新的 Goroutine 失去了對既有的 Goroutine 本地存儲。雖然你能夠保證本身的代碼不會產生其餘的 Goroutine。
    • 通常來講,你不能確保標準庫或任何第三方代碼不會這樣作。
  • Go 應用程序的複雜度和心智負擔等上升。

濫用的場景

有一個對外提供 HTTP 服務的 Go 應用,也就是 Web Server。Go HTTP Server 都是採起每次請求新起一個協程的方式。

假設能夠經過 GoroutineID 進行跨協程操縱,那麼就有可能出現個人 Goroutine,不必定是由 「我」 本身決定的。可能其餘正在處理的 GoroutineB 悄悄摸摸的改了我這個 GoroutineA 的行爲。

這就有可能致使一個災難問題,就是出問題時,你不知道是誰動了你的奶酪。查起問題來簡直就是一個災難。

如果本身維護的模塊清楚還起碼知道這事,假設你的前同事恰好離職了,你又在熟悉代碼,一出問題。這鍋那是死死的扣在了你的頭上了。

如何獲取 GoroutineID

剛剛咱們提到是在明面上把 GoroutineID 給隱藏了,那暗面呢,是否是有其餘辦法能夠獲取到?

答案是:能夠的。

經過駭客代碼的方式能夠獲取到。在 Go 語言的標準庫 http/2 的 gotrack 中,就有提供以下獲取方法:

func main() {
    go func() {
        fmt.Println("腦子進煎魚了的 GoroutineID:", curGoroutineID())
    }()

    time.Sleep(time.Second)
}

func curGoroutineID() uint64 {
    bp := littleBuf.Get().(*[]byte)
    defer littleBuf.Put(bp)
    b := *bp
    b = b[:runtime.Stack(b, false)]
    // Parse the 4707 out of "goroutine 4707 ["
    b = bytes.TrimPrefix(b, goroutineSpace)
    i := bytes.IndexByte(b, ' ')
    if i < 0 {
        panic(fmt.Sprintf("No space found in %q", b))
    }
    b = b[:i]
    n, err := parseUintBytes(b, 10, 64)
    if err != nil {
        panic(fmt.Sprintf("Failed to parse goroutine ID out of %q: %v", b, err))
    }
    return n
}

var littleBuf = sync.Pool{
    New: func() interface{} {
        buf := make([]byte, 64)
        return &buf
    },
}

var goroutineSpace = []byte("goroutine ")

輸出結果爲:

腦子進煎魚了的 GoroutineID: 18

結合 curGoroutineID 方法來看,能夠經過對 Go 運行時的分析,也就是 runtime.Stack 從而獲得 GoroutineID。

其做用,更多的是對進行跟蹤和調試做用居多。由於官方並無根據 GoroutineID 提供一系列跨協程操縱的方法。

也有以下開源庫能夠用於獲取 GoroutineID(不過均多年未維護了):

Go 團隊的 Dave Cheney 對其所開源的 GoroutineID 庫,評價:「If you use this package, you will go straight to hell.」:

davecheney/junk

也就是 「若是你使用這個包,你會直接下地獄。「,很是猛了,深深地勸退你們使用。

平常在哪裏常見

若是你們常常作救火隊長,去排查 Go 工程中的問題,例如:錯誤堆棧信息、PProf 性能分析等調試信息。

所以常常看到 GoroutineID,也就是 「goroutine #### […]」。

咱們所看到的 #### 就是真實的 GoroutineID,剩餘的信息就是一些堆棧跟蹤和錯誤描述了。

應該使用 GoroutineID 嗎?

從結果來看,確定是不推薦使用 GoroutineID 了。畢竟沒有什麼特別的好處,Go 團隊也是反對的。

因此通常都會直接回答 」沒法獲取 GoroutineID「,應當跟從語言設計理念,使用 Share Memory By Communicating 來實現跨協程的操縱會更合理。

總結

今天這篇文章咱們根據 GoroutineID 的歷史,做用,緣由,駭客方法進行了逐一梳理,摸索了下里面究竟爲什麼物。

進程、線程、協程的對比是一個面試中常被拿出來問的話題,而 GoroutineID 就是其中一點,這涉及到整個全局上的設計考慮。

你又是否遇到過 GoroutineID 使用和疑問的場景呢,歡迎你們一塊兒留言討論。

文章持續更新,能夠微信搜【腦子進煎魚了】閱讀,回覆【 000】有我準備的一線大廠面試算法題解和資料;本文 GitHub eddycjy/blog 已收錄,歡迎 Star 催更。
相關文章
相關標籤/搜索