擷取Goroutine Id的最佳實務

來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。

序言

在C/C++/Java等語言中,我們可以直接擷取Thread Id,然後通過映射Thread Id和二級調度Task Id的關係,可以在日誌中列印當前的TaskId,即使用者不感知Task Id的列印,適配層統一封裝,這使得多線程並發的日誌的查看或過濾變得非常容易。

Goroutine是Golang中輕量級線程的實現,由Go Runtime管理。Golang在語言層級支援輕量級線程,叫攜程。Golang標準庫提供的所有系統叫用作業(當然也包括所有同步IO操作),都會出讓CPU給其他Goroutine。這讓事情變得非常簡單,讓輕量級線程的切換管理不依賴於系統的線程和進程,也不依賴於CPU的核心數量。

Goroutine非常亮眼,但是自從go1.4版本以後,Goroutine Id無法直接從Go Runtime擷取了。

這是Golang的開發人員故意為之,避免開發人員濫用Goroutine Id實現Goroutine Local Storage(類似java的Thread Local Storage), 因為Goroutine Local Storage很難進行記憶體回收。因此儘管Go1.4之前暴露出了相應的方法,現在已經把它隱藏了。

這個決策有點因噎廢食,對於高並發日誌的查看和過濾就變得比較困難。儘管在日誌中可以使用業務本身的Id,但是在很多函數中僅僅為了列印而增加一些入參對於追求Clean Code的程式員實在無法接受。

筆者在本文中將找出一種簡單高效穩定的解決方案,並給出最佳實務。

既有的幾種方法

通過彙編擷取

複雜度高,位移地址隨版本可能有變化,不建議使用

通過第三方庫擷取

相關的第三方庫可以在github上找,比如:

https://github.com/jtolds/glshttps://github.com/huandu/goroutine

穩定性未知,效能也不高,不建議使用

通過runtime.Stack擷取

它利用runtime.Stack的堆棧資訊,將當前的堆棧資訊寫入到一個slice中,堆棧的第一行為 “goroutine #### […”,其中“####”就是當前的Goroutine Id,通過這個花招就可以實現Goid函數了。

採用該方法時,Goid函數的實現如下:

func Goid() int {    defer func()  {        if err := recover(); err != nil {            fmt.Println("panic recover:panic info:%v", err)        }    }()    var buf [64]byte    n := runtime.Stack(buf[:], false)    idField := strings.Fields(strings.TrimPrefix(string(buf[:n]), "goroutine "))[0]    id, err := strconv.Atoi(idField)    if err != nil {        panic(fmt.Sprintf("cannot get goroutine id: %v", err))    }    return id}

通過修改編譯器源碼擷取

在go源碼runtime包中增加函數Goid,直接調用runtime的getg函數擷取,具有簡單高效穩定的優點,同時每個團隊可以通過容器來部署自己的微服務。

該方法將在“最佳實務”一節中詳述。

方法三和方法四比較

分別採用方法三和方法四,將Goid函數連續調用10000次的效能資料如下:

方法三 方法四
> 50ms < 5us

對於方法三,擷取堆棧資訊會影響效能,所以建議對效能不敏感的情境採用;
對於方法四,直接調用runtime的getg函數擷取,效率最高,所以建議對效能有苛刻要求的情境採用。

本文關注效能,所以採用方法四。

最佳實務

下載go1.4版本的編譯器

在Golang的官方網站下載go1.4版本的編譯器,URL如下:

https://golang.org/dl/

解壓縮,將go檔案夾rename成go1.4,然後移動到$HOME目錄下。

修改go1.7.3版本的編譯器代碼

在Golang的官方網站下載go1.7.3版本的源碼。

編輯src/runtime/proc.go檔案,在尾部添加函數Goid:

func Goid() int64 {    _g_ := getg()    return _g_.goid}

運行src/make.bash命令(預設使用$HOME/go1.4目錄下的編譯器),編譯go1.7.3的新版本。

編譯完成後,將go檔案夾拷貝到GOROOT目錄下,使之生效:

$ go versiongo version go1.7.3 linux/amd64

測試代碼

我們類比一個完全可以並行的計算任務:計算N個整型數的總和。我們可以將所有整型數分成M份,M即CPU的個數。讓每個CPU開始計算分給它的那份計算任務,最後將每個CPU的計算結果再做一次累加,這樣就可以得到所有N個整型數的總和,實現代碼如下:

type Vector []intfunc (v Vector) DoSome(i, n int, u Vector, c chan int, add *int) int {    for ; i < n; i++ {        *add += u[i]    }    id := runtime.Goid(id)    fmt.Println("id:", id)    c <- 1    return 1}const NCPU = 16func (v Vector) DoAll(u Vector) int {    c := make(chan int, NCPU)    var add [NCPU]int    sum := 0    for i := 0; i < NCPU; i++ {        go v.DoSome(i * len(v) / NCPU, (i + 1)* len(v) / NCPU, u, c, &add[i])    }    for i := 0; i < NCPU; i++ {        <- c    }    for i := 0; i < NCPU; i++ {        sum += add[i]    }    return sum}func main() {    x := 0    y := 0    v := make(Vector, 160)    for i := 0; i < 160; i++ {        v[i] = i        x += i    }    y = v.DoAll(v)    fmt.Println("x =", x, "and y =", y)}

日誌

通過查看日誌,我們已將成功擷取到了Goroutine Id。一個字,完美!

id: 20id: 13id: 7id: 12id: 14id: 9id: 5id: 17id: 16id: 10id: 6id: 15id: 18id: 19id: 8id: 11x = 12720 and y = 12720

適配層封裝

我們可以將glog等第三方庫的日誌介面進行簡單封裝,隱藏goid的擷取和列印過程,使得使用者輕鬆。

小結

本文針對Golang中Goroutine的高並發的日誌難以查看或過濾的問題,分析了既有的幾種擷取Goroutine Id的方法,最後找到一種簡單高效穩定的方法,即通過修改編譯器源碼擷取,並給出了最佳實務,希望對讀者有一定的協助。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.