前言
golang官方默认是不支持tls(Thread Local Storage)的。支持tls的语言也不多,比如C++和GCC-C(gcc的c语言扩展,用于linux)。go官方拒绝实现tls,而为了解决tls问题,引入了上下文context包(闭包一定程度也是一种上下文)。通过上下文参数传递的方式来传递本地变量。这种方式好处就是显式上下文明确,坏处就是每个函数都要多一个上下文参数,传来传去的。黑客当然不会满足于此,于是乎一个哥们开源了go-tls
https://github.com/huandu/go-tls
代码不复杂,使用也比较简单。参考它的READ即可。
go-tls原理
简单介绍一下实现原理
它为每个协程定义了全局唯一的递增id,以及定义了一个全局的map用于存储每个协程的数据。map的key是协程的g结构地址,如此保证了数据是局部于协程的。由于map是全局的,多个协程会同时访问,属于临界区,因此添加了读写锁。如下定义
var (
tlsDataMap = map[unsafe.Pointer]*tlsData{}
tlsMu sync.RWMutex
tlsUniqueID int64
)
通过Get/Set/Del接口来获取、添加和删除协程的数据。
协程的数据可能需要在协程结束时进行释放,go-tls引入了退出函数列表,遵循FILO规则执行。
type tlsData struct {
id int64
data dataMap
atExitFuncs []func()
}
通过AtExit接口添加退出处理器。
然后获取协程的g通过汇编实现的
TEXT ·getg(SB), NOSPLIT, $0-8
get_tls(CX) //MOVQ TLS, CX
MOVQ g(CX), AX //0(CX)(TLS*1)
MOVQ AX, ret+0(FP)
RET
通过特殊的黑客技术替换了go的默认goexit为自定义hackedGoexit,以实现上面atExitFuncs 的调用。参见goexit.go
如此实现了协程的tls。可以一定程度上避免context的使用,当然这是和官方的声称不一致的。
风险
- 要考虑它的兼容性,即go编译器后期是否有这方面的更新。因为它使用了一些黑客技术,比如替换goexit的过程,这个和底层是强依赖的。
- 由于使用了读写锁,对性能敏感的需求请慎重。可能你会建议使用sync.Map替换普通map+读写锁的方式,其实我告诉你,个人已经单机压测过,它们两个性能相当,甚至后者比sync.Map还好那么一点点。
- 考虑维护成本;因为官方推荐context;
- 协程里起协程是无法传递tls的。
如果对以上四点不敏感的需求场景,可以放心使用。