Java的ThreadLocal是Java爲每一個線程提供的專用存儲,把一些信息放在ThreadLocal上,能夠用於來簡化上層應用的API使用。一個顯著的應用場景是,有了ThreadLocal後,就不須要在調用棧裏的每一個函數上都增長額外的參數來傳遞一些與調用鏈和日誌鏈路追蹤相關的信息了。git
Go Team 針對增長LocalStorage的提案,明確說明過,他們更推薦顯式地使用 Context 參數而不是使用LocalStorage來進行上下文信息的傳遞。社區裏卻是有幾個GLS(Goroutine Local Storage)的實現方案,咱們團隊也在系統裏使用了GLS,應用後並無明顯的性能下降,主要仍是不想在每一個函數定義上都添加參數來傳遞用來作日誌鏈路追蹤的TraceId,可是並不建議業務邏輯依賴這些三方的GLS庫。github
關因而否須要增長GLS的討論以及GLS帶來的性能和不兼容問題仍是挺多的,正好看到一篇文章對 Go 語言是否該引入GLS的討論進行了總結,在這裏分享給你們。golang
原文做者:蘭陵子安全
原文連接:lanlingzi.cn/post/techni…markdown
最近在設計調用鏈與日誌跟蹤的API,發現相比於Java與C++,Go語言中沒有原生的線程(協程)上下文,也不支持TLS(Thread Local Storage),更沒有暴露API獲取Goroutine的Id(後面簡稱GoId
)。這致使沒法像Java同樣,把一些信息放在TLS上,用於來簡化上層應用的API使用:不須要在調用棧的函數中經過傳遞參數來傳遞調用鏈與日誌跟蹤的一些上下文信息。併發
在Java與C++中,TLS是一種機制,指存儲在線程環境內的一個結構,用來存放該線程內獨享的數據。進程內的線程不能訪問不屬於本身的TLS,這就保證了TLS內的數據在線程內是全局共享的,而對於線程外倒是不可見的。函數
在Java中,JDK庫提供Thread.CurrentThread()
來獲取當前線程對象,提供ThreadLocal
來存儲與獲取線程局部變量。因爲Java能經過Thread.CurrentThread()
獲取當前線程,其實現的思路就很簡單了,在ThreadLocal類中有一個Map,用於存儲每個線程的變量。oop
ThreadLocal的API提供了以下的4個方法:post
public T get()
protected T initialValue()
public void remove()
public void set(T value)
複製代碼
T get()
:返回此線程局部變量的當前線程副本中的值,若是這是線程第一次調用該方法,則建立並初始化此副本。protected T initialValue()
: 返回此線程局部變量的當前線程的初始值。最多在每次訪問線程來得到每一個線程局部變量時調用此方法一次,即線程第一次使用get()
方法訪問變量的時候。若是線程先於get
方法調用set(T)
方法,則不會在線程中再調用initialValue
方法。void remove()
: 移除此線程局部變量的值。這可能有助於減小線程局部變量的存儲需求。若是再次訪問此線程局部變量,那麼在默認狀況下它將擁有其 initialValue
。void set(T value)
將此線程局部變量的當前線程副本中的值設置爲指定值。許多應用程序不須要這項功能,它們只依賴於initialValue()
方法來設置線程局部變量的值。在Go語言中,而Google提供的解決方法是採用golang.org/x/net/context
包來傳遞GoRoutine的上下文。對Go的Context的深刻了解可參考我以前的分析:理解Go Context機制。Context
也是能存儲Goroutine一些數據達到共享,但它提供的接口是WithValue
函數來建立一個新的Context
對象。性能
func WithValue(parent Context, key interface{}, val interface{}) Context {
return &valueCtx{parent, key, val}
}
type valueCtx struct {
Context
key, val interface{}
}
func (c *valueCtx) Value(key interface{}) interface{} {
if c.key == key {
return c.val
}
return c.Context.Value(key)
}
複製代碼
從上面代碼中能夠看出,Context
設置一次Value,就會產生一個Context
對象,獲取Value是先找當前Context
存儲的值,若沒有再向父一級查找。獲取Value
能夠說是多Goroutine訪問安全,由於它的接口設計上,是隻一個Goroutine一次設置Key/Value
,其它多Goroutine只能讀取Key
的Value
。
This, among other reasons, to prevent programmers for simulating thread local storage using the goroutine id as a key.
官方說,就爲了不採用Goroutine Id
當成Thread Local Storage
的Key
。
Please don’t use goroutine local storage. It’s highly discouraged. In fact, IIRC, we used to expose Goid, but it is hidden since we don’t want people to do this.
用戶常用GoId來實現goroutine local storage
,而Go語言不但願用戶使用goroutine local storage
。
when goroutine goes away, its goroutine local storage won’t be GCed. (you can get goid for the current goroutine, but you can’t get a list of all running goroutines)
不建議使用goroutine local storage
的緣由是因爲不容易GC,雖然能獲當前的GoId,但不能獲取其它正在運行的Goroutine。
what if handler spawns goroutine itself? the new goroutine suddenly loses access to your goroutine local storage. You can guarantee that your own code won’t spawn other goroutines, but in general you can’t make sure the standard library or any 3rd party code won’t do that.
另外一個重要的緣由是因爲產生一個Goroutine很是地容易(而線程通用會採用線程池),新產生的Goroutine會失去訪問goroutine local storage
。須要上層應用保證不會產生新的Goroutine,但咱們很難確保標準庫或第三庫不會這樣作。
thread local storage is invented to help reuse bad/legacy code that assumes global state, Go doesn’t have legacy code like that, and you really should design your code so that state is passed explicitly and not as global (e.g. resort to goroutine local storage)
TLS的應用是幫助重用現有那些很差(遺留)的採用全局狀態的代碼。而Go語言建議是從新設計代碼,採用顯示地傳遞狀態而不是採用全局狀態(例如採用goroutine local storage
)。
雖然Go語言有意識地隱藏GoId,但目前仍是有手段來獲取GoId:
修改源代碼暴露GoId,但Go語言可能隨時修改源碼,致使不兼容
在標準庫的runtime/proc.go
(Go 1.6.3)中的newextram
函數,會產生個GoId:
mp.lockedg = gp
gp.lockedm = mp
gp.goid = int64(atomic.Xadd64(&sched.goidgen, 1))
複製代碼
經過runtime.Stack
來分析Stack輸出信息獲取GoId。
在標準庫的runtime/mprof.go
(Go 1.6.3)中,runtime.Stack
會獲取gp對象(包含GoId)並輸出整個Stack信息:
func Stack(buf []byte, all bool) int {
if all {
stopTheWorld("stack trace")
}
n := 0
if len(buf) > 0 {
gp := getg()
sp := getcallersp(unsafe.Pointer(&buf))
pc := getcallerpc(unsafe.Pointer(&buf))
systemstack(func() {
g0 := getg()
g0.m.traceback = 1
g0.writebuf = buf[0:0:len(buf)]
goroutineheader(gp)
traceback(pc, sp, 0, gp)
if all {
tracebackothers(gp)
}
g0.m.traceback = 0
n = len(g0.writebuf)
g0.writebuf = nil
})
}
if all {
startTheWorld()
}
return n
}
複製代碼
從文件名就能夠看出,runtime/mprof.go
是用於作Profile分析,獲取Stack確定性能不會太好。從上面的代碼來看,若第二個參數指定爲true,還會STW,業務系統不管如何都沒法接受。若Go語言修改了Stack的輸出,分析Stack信息也會致使沒法正常獲取GoId。
通用runtime.Callers
來給調用Stack來打標籤
經過內聯c或者內聯彙編
go版本1.5,x86_64arc下彙編,估計也不通用
// func GoID() int64
TEXT s3lib GoID(SB),NOSPLIT,$0-8
MOVQ TLS, CX
MOVQ 0(CX)(TLS*1), AX
MOVQ AX, ret+0(FP)
RET
複製代碼
只要有機制獲取GoId,就能夠像Java同樣來採用全局的map實現goroutine local storage
,在Github上搜索一下,發現有兩個:
GoId是經過runtime.Stack
來分析Stack輸出信息獲取GoId。
GoId是通用runtime.Callers
來給調用Stack來打標籤
第二個有人在2013年測試過性能,數據以下:
BenchmarkGetValue 500000 2953 ns/op BenchmarkSetValues 500000 4050 ns/op
上面的測試結果看似還不錯,但goroutine local storage
實現無外乎是map+RWMutex
,存在性能瓶頸:
無論怎麼樣,沒有官方的GLS,的確不是很方便,第三方實現又存在性能與不兼容風險。連jtolds/gls
做者也貼出其它人的評價:
「Wow, that’s horrifying.」
「This is the most terrible thing I have seen in a very long time.」
「Where is it getting a context from? Is this serializing all the requests? What the heck is the client being bound to? What are these tags? Why does he need callers? Oh god no. No no no.」
Go語言官方認爲TLS來存儲全局狀態是很差的設計,而是要顯示地傳遞狀態。Google給的解決方法是golang.org/x/net/context
。