Java的ThreadLocal是Java为每个线程提供的专用存储,把一些信息放在ThreadLocal上,可以用于来简化上层应用的API使用。一个显著的应用场景是,有了ThreadLocal后,就不需要在调用栈里的每个函数上都增加额外的参数来传递一些与调用链和日志链路追踪相关的上下文信息了。

Go Team 针对增加LocalStorage的提案,明确说明过,他们更推荐显式地使用 Context 参数而不是使用LocalStorage来进行上下文信息的传递。社区里倒是有几个GLS(Goroutine Local Storage)的实现方案,我们团队也在系统里使用了GLS,应用后并没有明显的性能降低,主要还是不想在每个函数定义上都添加参数来传递用来做日志链路追踪的TraceId,但是并不建议业务逻辑依赖这些三方的GLS库

关于是否需要增加GLS的讨论以及GLS带来的性能和不兼容问题还是挺多的,正好看到一篇文章对 Go 语言是否该引入GLS的讨论进行了总结,在这里分享给大家。

原文作者:兰陵子

原文链接:http://lanlingzi.cn/post/technical/2016/0813_go_gls/

背景

最近在设计调用链与日志跟踪的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,用于存储每一个线程的变量。

ThreadLocal的API提供了如下的4个方法:

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 {Contextkey, 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只能读取KeyValue

为什么无获取GoId接口

This, among other reasons, to prevent programmers for simulating thread local storage using the goroutine id as a key.

官方说,就为了避免采用Goroutine Id当成Thread Local StorageKey

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)。

其它手段获取GoId

虽然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 := 0if len(buf) > 0 {gp := getg()sp := getcallersp(unsafe.Pointer(&buf))pc := getcallerpc(unsafe.Pointer(&buf))systemstack(func() {g0 := getg()g0.m.traceback = 1g0.writebuf = buf[0:0:len(buf)]goroutineheader(gp)traceback(pc, sp, 0, gp)if all {tracebackothers(gp)}g0.m.traceback = 0n = 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来打标签

    代码参考:https://github.com/jtolds/gls/blob/master/stack_tags_main.go#L43

  • 通过内联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
    

开源goroutine local storage实现

只要有机制获取GoId,就可以像Java一样来采用全局的map实现goroutine local storage,在Github上搜索一下,发现有两个:

  • tylerb/gls

    GoId是通过runtime.Stack来分析Stack输出信息获取GoId。

  • jtolds/gls

    GoId是通用runtime.Callers来给调用Stack来打标签

第二个有人在2013年测试过性能,数据如下:

BenchmarkGetValue 500000 2953 ns/op

BenchmarkSetValues 500000 4050 ns/op

上面的测试结果看似还不错,但goroutine local storage实现无外乎是map+RWMutex,存在性能瓶颈:

  • Goroutine不像Thread,它的个数可以上十万并发,当这么多的Goroutine同时竞争同一把锁时,性能会急剧恶化。

  • GoId是通过分析调用Stack的信息来获取,也是一个高成本的调用,一个字:慢。

不管怎么样,没有官方的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

Goroutine Local Storage的一些实现方案和必要性讨论相关推荐

  1. SAP Spartacus 如何使用 API 从浏览器 local Storage 读取数据

    如下图所示,SAP 电商云 UI,用户的购物车 ID,持久化在浏览器的 local storage 里: 运行时,通过封装好的函数 getStorage 读取: 为什么会触发 State module ...

  2. 使用浏览器的 Local Storage 真的安全吗?

    LocalStorage 是一个 HTML5 网络存储对象,用于将数据存储在客户端--即本地,在用户的计算机上. 本地存储的数据没有到期日期,并且会一直存在,直到被删除. (相比之下,会话存储是另一个 ...

  3. SAP Spartacus 用户登录成功后,Access Token 持久化到浏览器 local storage 的执行原理

    下图第1487行代码,调用Angular HTTP library,往this.tokenEndpoint指向的API发送HTTP post请求,参数为用户在login form里输入的用户名和密码: ...

  4. SAP Spartacus 在未登录状态下浏览器 local storage 里存储的数据

    url: http://localhost:4200/powertools-spa/en/USD/ 未登录状态下: 观察local storage存储的数据: auth: {token: {}, us ...

  5. SAP UI5 应用的调试标志位的本地存储逻辑 - local storage 使用的一个例子

    We know that once we enable debug mode via "Ctrl+Alt+Shift+P", this setting will be persis ...

  6. 关于HTML5本地持久化存储的Web SQL、Local Storage、Cookies技术

    在浏览器客户端记录一些信息,有三种常用的Web数据持久化存储的方式,分别是Web SQL.Local Storage.Cookies. Web SQL 作为html5本地数据库,可通过一套API来操纵 ...

  7. 【JEECG技术博文】Local storage easyui extensions

    1. Local storage背景 cookie弊端:同域内http请求都会带cookie,增加带宽和流量:有个数和大小限制(约4K). 在HTML5中,本地存储是一个window的属性,包括loc ...

  8. html5 删除llocalstorage变量,删除存储在浏览器中的 Local Storage 数据《 HTML5:Web 存储 》...

    设置数据用的是 setItem ,获取到设置的数据用的是 getItem .... 如果想要去移除设置的数据 .. 可以使用 localStorage 的 removeItem() 这个方法 - 比如 ...

  9. html5 dom storage,Client-side data storing,DOM storage or HTML5 Local Storage?

    问题 Im really confused when thinking about my requirement to store data locally for offline viewing.N ...

最新文章

  1. Memcached 缓存系统的-介绍、安装以及应用
  2. python3 异步 async with 用法
  3. The 'Microsoft Jet OLEDB 4.0 Provider' is not registered on the local machine
  4. log4j日志文件配置
  5. 多对一!分组查询!MySQL分组函数,聚合函数,分组查询
  6. linux mysql 修改字符集_linux下mysql修改字符集,远程连接
  7. 数仓备机DN重建:快速修复你的数仓DN单点故障
  8. 2020 及以后的八大最显著技术趋势!
  9. [C++]虚函数-同名访问
  10. Wincc语音报警 Wincc真人声音报警
  11. Java JDK8下载 (jdk-8u251-windows-x64和jdk-8u271-linux-x64.tar)
  12. 从 NASL 说开:低代码编程语言能饭否
  13. 文书档案管理系统服务器版,创奇文书档案管理系统客户端官方版
  14. 运动无线耳机哪款不容易掉、最不容易脱落的蓝牙耳机推荐
  15. 2019中国信息安全自主可控行业政策盘点及网络安全行业分析
  16. connect ETIMEDOUT......
  17. 全志A64平台 TP9950 BT656输入驱动调试(1)环境搭建驱动编写
  18. JAVA基础之java语法
  19. mangos--e品魔兽世界,一个纪念集!
  20. 安卓毕业设计app项目基于Uniapp+SSM实现的安卓的掌上校园系统食堂缴费图书馆预约

热门文章

  1. 为5—18岁青少年提供营地教育,漫族完成百万级天使轮融资
  2. jQuery的无new实例化
  3. RssTookit使用小结
  4. 迷你版Spring MVC 实现
  5. 【leetcode】390. Elimination Game
  6. 改造房车走天下,这个阿里妹子不一般
  7. SpringBoot入门系列: Spring Boot的测试
  8. Your password has expired. To log in you must change it using a client that supports expired pass...
  9. Word2013中制作按钮控件
  10. 浅谈line-height