一切皆有可能——Golang 中的”ThreadLocal“庫
作者:sisyphsu
來源:SegmentFault 思否社區
開源倉庫:
https://github.com/go-eden/routine
本文介紹的是新寫的routine
庫,它封裝並提供了一些易用、高性能的goroutine
上下文訪問接口,可以幫助你更優雅地訪問協程上下文信息,但你也可能就此打開了潘多拉魔盒。
介紹
Golang
語言從設計之初,就一直在不遺餘力地向開發者屏蔽協程上下文的概念,包括協程goid
的獲取、進程內部協程狀態、協程上下文存儲等。
如果你使用過其他語言如C++/Java
等,那麼你一定很熟悉ThreadLocal
,而在開始使用Golang
之後,你一定會爲缺少類似ThreadLocal
的便捷功能而深感困惑與苦惱。當然你可以選擇使用Context
,讓它攜帶着全部上下文信息,在所有函數的第一個輸入參數中出現,然後在你的系統中到處穿梭。
而routine
的核心目標就是開闢另一條路:將goroutine local storage
引入Golang
世界,同時也將協程信息暴露出來,以滿足某些人可能有的需求。
使用演示
此章節簡要介紹如何安裝與使用routine
庫。
安裝
go get github.com/go-eden/routine
使用goid
以下代碼簡單演示了routine.Goid()
與routine.AllGoids()
的使用:
package main
import (
"fmt"
"github.com/go-eden/routine"
"time"
)
func main() {
go func() {
time.Sleep(time.Second)
}()
goid := routine.Goid()
goids := routine.AllGoids()
fmt.Printf("curr goid: %d\n", goid)
fmt.Printf("all goids: %v\n", goids)
}
此例中main
函數啓動了一個新的協程,因此Goid()
返回了主協程1
,AllGoids()
返回了主協程及協程18
:
curr goid: 1
all goids: [1 18]
使用LocalStorage
以下代碼簡單演示了LocalStorage
的創建、設置、獲取、跨協程傳播等:
package main
import (
"fmt"
"github.com/go-eden/routine"
"time"
)
var nameVar = routine.NewLocalStorage()
func main() {
nameVar.Set("hello world")
fmt.Println("name: ", nameVar.Get())
// 其他協程不能讀取前面Set的"hello world"
go func() {
fmt.Println("name1: ", nameVar.Get())
}()
// 但是可以通過Go函數啓動新協程,並將當前main協程的全部協程上下文變量賦值過去
routine.Go(func() {
fmt.Println("name2: ", nameVar.Get())
})
// 或者,你也可以手動copy當前協程上下文至新協程,Go()函數的內部實現也是如此
ic := routine.BackupContext()
go func() {
routine.InheritContext(ic)
fmt.Println("name3: ", nameVar.Get())
}()
time.Sleep(time.Second)
}
執行結果爲:
name: hello world
name1: <nil>
name3: hello world
name2: hello world
API 文檔
=============
此章節詳細介紹了routine
庫封裝的全部接口,以及它們的核心功能、實現方式等。
Goid() (id int64)
獲取當前goroutine
的goid
。
在正常情況下,Goid()
優先嚐試通過go_tls
的方式直接獲取,此操作性能極高,耗時通常只相當於rand.Int()
的五分之一。
若出現版本不兼容等錯誤時,Goid()
會嘗試降級,即從runtime.Stack
信息中解析獲取,此時性能會急劇下降約千倍,但它可以保證功能正常可用。
AllGoids() (ids []int64)
獲取當前進程全部活躍goroutine
的goid
。
在go 1.15
及更舊的版本中,AllGoids()
會嘗試從runtime.Stack
信息中解析獲取全部協程信息,但此操作非常低效,非常不建議在高頻邏輯中使用。
在go 1.16
之後的版本中,AllGoids()
會通過native
的方式直接讀取runtime
的全局協程池信息,在性能上得到了極大的提高, 但考慮到生產環境中可能有萬、百萬級的協程數量,因此仍不建議在高頻使用它。
NewLocalStorage()
:
創建一個新的LocalStorage
實例,它的設計思路與用法和其他語言中的ThreadLocal
非常相似。
BackupContext() *ImmutableContext
備份當前協程上下文的local storage
數據,它只是一個便於上下文數據傳遞的不可變結構體。
InheritContext(ic *ImmutableContext)
主動繼承備份到的上下文local storage
數據,它會將其他協程BackupContext()
的數據複製入當前協程上下文中,從而支持跨協程的上下文數據傳播。
Go(f func())
啓動一個新的協程,同時自動將當前協程的全部上下文local storage
數據複製至新協程,它的內部實現由BackupContext()
和InheritContext()
組成。
LocalStorage
表示協程上下文變量,支持的函數包括:
-
Get() (value interface{})
:獲取當前協程已設置的變量值,若未設置則爲nil
-
Set(v interface{}) interface{}
:設置當前協程的上下文變量值,返回之前已設置的舊值 -
Del() (v interface{})
:刪除當前協程的上下文變量值,返回已刪除的舊值 -
Clear()
:徹底清理此上下文變量在所有協程中保存的舊值
提示:Get/Set/Del
的內部實現採用無鎖設計,在大部分情況下,它的性能表現都應該非常穩定且高效。
垃圾回收
routine
庫內部維護了全局的storages
變量,它存儲了全部協程的上下文變量信息,在讀寫時基於協程的goid
和協程變量的ptr
進行變量尋址映射。
在進程的整個生命週期中,它可能會創建於銷燬無數個協程,那麼這些協程的上下文變量如何清理呢?
爲解決這個問題,routine
內部分配了一個全局的GCTimer
,此定時器會在storages
需要被清理時啓動,定時掃描並清理dead
協程在storages
中緩存的上下文變量,從而避免可能出現的內存泄露隱患。
License
MIT
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/CPthQSqUIO-u-VkTQSMVXA