首頁 > 後端開發 > Golang > 如何取得Goroutine ID?

如何取得Goroutine ID?

Mary-Kate Olsen
發布: 2025-01-04 10:45:35
原創
146 人瀏覽過

How to Get the Goroutine ID?

在作業系統中,每個行程都有一個唯一的行程ID,每個執行緒也有自己唯一的執行緒ID。同樣,在Go語言中,每個Goroutine都有自己唯一的Go例程ID,這在panic等場景中經常遇到。雖然Goroutine有固有的ID,但是Go語言故意不提供取得這個ID的介面。這次我們將嘗試透過Go彙編語言來取得Goroutine ID。

1. 官方沒有goid的設計(https://github.com/golang/go/issues/22770)

根據官方相關資料,Go語言故意不提供goid的原因是為了避免濫用。因為大多數使用者在輕鬆獲得goid之後,在後續的程式設計中會不自覺地寫出強烈依賴goid的程式碼。對 goid 的強烈依賴會導致該程式碼難以移植,同時也會使並發模型變得複雜。同時,Go語言中可能存在大量的Goroutine,但每個Goroutine何時被銷毀並不容易即時監控,這也會導致依賴goid的資源無法自動回收(需要人工回收)。不過,如果你是 Go 組譯語言用戶,你完全可以忽略這些擔憂。

注意:如果強行獲得goid,可能會被「羞辱」? :
https://github.com/golang/go/blob/master/src/runtime/proc.go#L7120

2. Pure Go中獲取goid

為了方便理解,我們先嘗試取得純Go中的goid。雖然純Go中取得goid的效能比較低,但程式碼具有良好的可移植性,也可以用來測試驗證其他方法所取得的goid是否正確。

每個Go語言使用者都應該知道panic函數。呼叫panic函數會導致Goroutine異常。如果在到達 Goroutine 的根函數之前,recover 函數沒有處理恐慌,則運行時將列印相關異常和堆疊資訊並退出 Goroutine。

讓我們建構一個簡單的例子,透過panic輸出goid:

package main

func main() {
    panic("leapcell")
}
登入後複製
登入後複製
登入後複製
登入後複製

運作後會輸出以下資訊:

panic: leapcell

goroutine 1 [running]:
main.main()
    /path/to/main.go:4 +0x40
登入後複製
登入後複製
登入後複製
登入後複製

我們可以猜測Panic輸出訊息goroutine 1 [running]中的1就是goid。但是我們要如何取得程式中panic的輸出資訊呢?其實上面的資訊只是當前函數呼叫堆疊幀的文字描述。 runtime.Stack函數提供了取得這些資訊的功能。

我們來重建一個基於runtime.Stack函數的例子,透過輸出目前堆疊幀的資訊來輸出goid:

package main

func main() {
    panic("leapcell")
}
登入後複製
登入後複製
登入後複製
登入後複製

運作後會輸出以下資訊:

panic: leapcell

goroutine 1 [running]:
main.main()
    /path/to/main.go:4 +0x40
登入後複製
登入後複製
登入後複製
登入後複製

所以,從runtime.Stack得到的字串中解析出goid資訊就很容易了:

package main

import "runtime"

func main() {
    var buf = make([]byte, 64)
    var stk = buf[:runtime.Stack(buf, false)]
    print(string(stk))
}
登入後複製
登入後複製
登入後複製

GetGoid 函數的細節我們不再贅述。要注意的是,runtime.Stack函數不僅可以取得目前Goroutine的堆疊訊息,還可以取得所有Goroutine的堆疊資訊(由第二個參數控制)。同時Go語言中的net/http2.curGoroutineID函數取得goid的方式也是類似。

3. 從g結構中取得goid

根據Go官方彙編語言文檔,每個正在運行的Goroutine結構體的g指針都儲存在目前運行的Goroutine所在系統線程的本地儲存TLS中。我們可以先取得TLS線程本地存儲,然後從TLS中獲取g結構體的指針,最後從g結構體中提取goid。

下面是透過引用runtime套件中定義的get_tls巨集來取得g指標:

goroutine 1 [running]:
main.main()
    /path/to/main.g
登入後複製
登入後複製

get_tls是runtime/go_tls.h頭檔中定義的巨集函數。

對於AMD64平台,get_tls宏函數定義如下:

import (
    "fmt"
    "strconv"
    "strings"
    "runtime"
)

func GetGoid() int64 {
    var (
        buf [64]byte
        n   = runtime.Stack(buf[:], false)
        stk = strings.TrimPrefix(string(buf[:n]), "goroutine")
    )

    idField := strings.Fields(stk)[0]
    id, err := strconv.Atoi(idField)
    if err!= nil {
        panic(fmt.Errorf("can not get goroutine id: %v", err))
    }

    return int64(id)
}
登入後複製
登入後複製

擴充get_tls巨集函數後,取得g指標的程式碼如下:

get_tls(CX)
MOVQ g(CX), AX     // Move g into AX.
登入後複製
登入後複製

其實TLS類似執行緒本地儲存的位址,該位址對應的記憶體中的資料就是g指標。我們可以再直接一點:

#ifdef GOARCH_amd64
#define        get_tls(r)        MOVQ TLS, r
#define        g(r)        0(r)(TLS*1)
#endif
登入後複製
登入後複製

基於上面的方法,我們可以封裝一個getg函數來取得g指標:

MOVQ TLS, CX
MOVQ 0(CX)(TLS*1), AX
登入後複製

然後,在Go程式碼中,透過goid成員在g結構體中的偏移量來取得goid的值:

MOVQ (TLS), AX
登入後複製

這裡,g_goid_offset是goid成員的偏移量。 g結構指的是runtime/runtime2.go。

Go1.10版本中,goid的偏移量為152位元組。所以,上面的程式碼只能在 goid 偏移量也是 152 位元組的 Go 版本中正確運行。根據偉大湯普森的神諭,枚舉和蠻力是解決所有難題的靈丹妙藥。我們也可以將 goid 偏移量保存在一個表中,然後根據 Go 版本號查詢 goid 偏移量。

以下是改進後的程式碼:

// func getg() unsafe.Pointer
TEXT ·getg(SB), NOSPLIT, <pre class="brush:php;toolbar:false">const g_goid_offset = 152 // Go1.10

func GetGroutineId() int64 {
    g := getg()
    p := (*int64)(unsafe.Pointer(uintptr(g) + g_goid_offset))
    return *p
}
登入後複製
-8 MOVQ (TLS), AX MOVQ AX, ret+0(FP) RET

現在,goid 偏移量終於可以自動適配已發布的 Go 語言版本了。

4、取得g結構體對應的介面對象

雖然枚舉和暴力破解很簡單,但它們不能很好地支援正在開發的未發布的 Go 版本。我們無法事先知道 goid 成員在某個正在開發的版本中的偏移量。

如果是在runtime套件內部,我們可以透過unsafe.OffsetOf(g.goid)直接取得該成員的偏移量。我們也可以透過反射來取得g結構體的類型,然後透過該類型查詢某個成員的偏移量。由於g結構體是內部類型,Go程式碼無法從外部套件取得g結構體的類型資訊。然而,在Go彙編語言中,我們可以看到所有的符號,所以理論上,我們也可以得到g結構體的類型資訊。

定義任何型別後,Go語言都會產生該型別對應的型別資訊。例如,g結構體會產生一個type·runtime·g標識符來表示g結構體的值類型訊息,並且也會產生一個type·*runtime·g標識符來表示指標類型資訊。如果 g 結構體有方法,那麼也會產生 go.itab.runtime.g 和 go.itab.*runtime.g 類型訊息,用方法來表示類型資訊。

如果我們能得到代表g結構類型的type·runtime·g和g指針,那麼我們就可以建構g物件的介面了。下面是改進後的getg函數,回傳g指針對象的介面:

package main

func main() {
    panic("leapcell")
}
登入後複製
登入後複製
登入後複製
登入後複製

這裡,AX暫存器對應g指針,BX暫存器對應g結構體的型別。然後,使用runtime·convT2E函數將類型轉換為介面。因為我們沒有使用g結構的指標類型,所以傳回的介面表示g結構的值類型。理論上我們也可以建構一個g指標類型的接口,但是由於Go彙編語言的限制,我們無法使用type·*runtime·g標識符。

根據g回傳的接口,很容易取得goid:

panic: leapcell

goroutine 1 [running]:
main.main()
    /path/to/main.go:4 +0x40
登入後複製
登入後複製
登入後複製
登入後複製

以上程式碼直接透過反射取得goid。理論上只要反射介面的名稱和goid成員沒有變化,程式碼就可以正常運作。經過實際測試,上述程式碼在Go1.8、Go1.9、Go1.10版本中均能正確運作。樂觀地講,如果g結構類型的名稱不改變,Go語言的反射機制不改變,應該也能在未來的Go語言版本中運作。

雖然反射有一定的彈性,但是反射的表現卻一直被詬病。一個改進的想法是透過反射來取得goid的偏移量,然後透過g指標和偏移量來取得goid,這樣反射只需要在初始化階段執行一次。

以下是g_goid_offset變數的初始化程式碼:

package main

import "runtime"

func main() {
    var buf = make([]byte, 64)
    var stk = buf[:runtime.Stack(buf, false)]
    print(string(stk))
}
登入後複製
登入後複製
登入後複製

得到正確的goid偏移量後,按照前面提到的方式取得goid:

package main

func main() {
    panic("leapcell")
}
登入後複製
登入後複製
登入後複製
登入後複製

至此,我們取得goid的實作想法已經夠完整,但是彙編程式碼仍有嚴重的安全隱患。

雖然 getg 函數被宣告為禁止使用 NOSPLIT 標誌進行堆疊拆分的函數類型,但 getg 函數內部呼叫了更複雜的runtime·convT2E 函數。如果runtime·convT2E函數遇到堆疊空間不足,可能會觸發堆疊分裂操作。當堆疊分裂時,GC會移動函數參數、傳回值和局部變數中的堆疊指標。然而,我們的 getg 函數並沒有提供局部變數的指標資訊。

以下是改進後的getg函數的完整實作:

panic: leapcell

goroutine 1 [running]:
main.main()
    /path/to/main.go:4 +0x40
登入後複製
登入後複製
登入後複製
登入後複製

這裡,NO_LOCAL_POINTERS 表示函數沒有局部指標變數。同時對傳回的介面進行零值初始化,初始化完成後再用GO_RESULTS_INITIALIZED通知GC。這樣可以保證當堆疊分裂時,GC能夠正確處理傳回值和局部變數中的指標。

5. goid的應用:本地存儲

有了goid,建置Goroutine本地儲存就變得非常簡單。我們可以定義一個 gls 套件來提供 goid 功能:

package main

import "runtime"

func main() {
    var buf = make([]byte, 64)
    var stk = buf[:runtime.Stack(buf, false)]
    print(string(stk))
}
登入後複製
登入後複製
登入後複製

gls 套件變數只是包裝了一個映射,並支援透過sync.Mutex 互斥體進行並發存取。

然後定義一個內部 getMap 函數來取得每個 Goroutine 位元組的映射:

goroutine 1 [running]:
main.main()
    /path/to/main.g
登入後複製
登入後複製

取得Goroutine的私有映射後,就是增刪改操作的正常介面:

import (
    "fmt"
    "strconv"
    "strings"
    "runtime"
)

func GetGoid() int64 {
    var (
        buf [64]byte
        n   = runtime.Stack(buf[:], false)
        stk = strings.TrimPrefix(string(buf[:n]), "goroutine")
    )

    idField := strings.Fields(stk)[0]
    id, err := strconv.Atoi(idField)
    if err!= nil {
        panic(fmt.Errorf("can not get goroutine id: %v", err))
    }

    return int64(id)
}
登入後複製
登入後複製

最後,我們提供了一個Clean函數來釋放Goroutine對應的map資源:

get_tls(CX)
MOVQ g(CX), AX     // Move g into AX.
登入後複製
登入後複製

這樣,一個極簡的Goroutine本地儲存gls物件就完成了。

以下是使用本機儲存的簡單範例:

#ifdef GOARCH_amd64
#define        get_tls(r)        MOVQ TLS, r
#define        g(r)        0(r)(TLS*1)
#endif
登入後複製
登入後複製

透過Goroutine本地存儲,不同層級的函數可以共享儲存資源。同時,為了避免資源洩漏,在Goroutine的根函數中,需要透過defer語句呼叫gls.Clean()函數來釋放資源。

Leapcell:用於託管 Golang 應用程式的高級無伺服器平台

How to Get the Goroutine ID?

最後推薦一個最適合部署Go服務的平台:leapcell

1. 多語言支持

  • 使用 JavaScript、Python、Go 或 Rust 進行開發。

2.免費部署無限個項目

  • 只需支付使用費用-無請求,不收費。

3. 無與倫比的成本效益

  • 即用即付,無閒置費用。
  • 範例:25 美元支援 694 萬個請求,平均回應時間為 60 毫秒。

4.簡化的開發者體驗

  • 直覺的使用者介面,輕鬆設定。
  • 完全自動化的 CI/CD 管道和 GitOps 整合。
  • 即時指標和日誌記錄以獲取可操作的見解。

5. 輕鬆的可擴充性和高效能

  • 自動擴充以輕鬆處理高並發。
  • 零營運開銷-只需專注於建置。

在文件中探索更多內容!

Leapcell Twitter:https://x.com/LeapcellHQ

以上是如何取得Goroutine ID?的詳細內容。更多資訊請關注PHP中文網其他相關文章!

來源:dev.to
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
作者最新文章
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板