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
。