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

