一切皆有可能——Golang 中的”ThreadLocal“库
作者:sisyphsu
来源:SegmentFault 思否社区
开源仓库:
https://github.com/go-eden/routine
本文介绍的是新写的routine
库,它封装并提供了一些易用、高性能的goroutine
上下文访问接口,可以帮助你更优雅地访问协程上下文信息,但你也可能就此打开了潘多拉魔盒。
介绍
Golang
语言从设计之初,就一直在不遗余力地向开发者屏蔽协程上下文的概念,包括协程goid
的获取、进程内部协程状态、协程上下文存储等。
如果你使用过其他语言如C++/Java
等,那么你一定很熟悉ThreadLocal
,而在开始使用Golang
之后,你一定会为缺少类似ThreadLocal
的便捷功能而深感困惑与苦恼。当然你可以选择使用Context
,让它携带着全部上下文信息,在所有函数的第一个输入参数中出现,然后在你的系统中到处穿梭。
而routine
的核心目标就是开辟另一条路:将goroutine local storage
引入Golang
世界,同时也将协程信息暴露出来,以满足某些人可能有的需求。
使用演示
此章节简要介绍如何安装与使用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
函数启动了一个新的协程,因此Goid()
返回了主协程1
,AllGoids()
返回了主协程及协程18
:
curr goid: 1
all goids: [1 18]
使用LocalStorage
以下代码简单演示了LocalStorage
的创建、设置、获取、跨协程传播等:
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())
// 其他协程不能读取前面Set的"hello world"
go func() {
fmt.Println("name1: ", nameVar.Get())
}()
// 但是可以通过Go函数启动新协程,并将当前main协程的全部协程上下文变量赋值过去
routine.Go(func() {
fmt.Println("name2: ", nameVar.Get())
})
// 或者,你也可以手动copy当前协程上下文至新协程,Go()函数的内部实现也是如此
ic := routine.BackupContext()
go func() {
routine.InheritContext(ic)
fmt.Println("name3: ", nameVar.Get())
}()
time.Sleep(time.Second)
}
执行结果为:
name: hello world
name1: <nil>
name3: hello world
name2: hello world
API文档
此章节详细介绍了routine
库封装的全部接口,以及它们的核心功能、实现方式等。
Goid() (id int64)
获取当前goroutine
的goid
。
在正常情况下,Goid()
优先尝试通过go_tls
的方式直接获取,此操作性能极高,耗时通常只相当于rand.Int()
的五分之一。
若出现版本不兼容等错误时,Goid()
会尝试降级,即从runtime.Stack
信息中解析获取,此时性能会急剧下降约千倍,但它可以保证功能正常可用。
AllGoids() (ids []int64)
获取当前进程全部活跃goroutine
的goid
。
在go 1.15
及更旧的版本中,AllGoids()
会尝试从runtime.Stack
信息中解析获取全部协程信息,但此操作非常低效,非常不建议在高频逻辑中使用。
在go 1.16
之后的版本中,AllGoids()
会通过native
的方式直接读取runtime
的全局协程池信息,在性能上得到了极大的提高, 但考虑到生产环境中可能有万、百万级的协程数量,因此仍不建议在高频使用它。
NewLocalStorage()
:
创建一个新的LocalStorage
实例,它的设计思路与用法和其他语言中的ThreadLocal
非常相似。
BackupContext() *ImmutableContext
备份当前协程上下文的local storage
数据,它只是一个便于上下文数据传递的不可变结构体。
InheritContext(ic *ImmutableContext)
主动继承备份到的上下文local storage
数据,它会将其他协程BackupContext()
的数据复制入当前协程上下文中,从而支持跨协程的上下文数据传播。
Go(f func())
启动一个新的协程,同时自动将当前协程的全部上下文local storage
数据复制至新协程,它的内部实现由BackupContext()
和InheritContext()
组成。
LocalStorage
表示协程上下文变量,支持的函数包括:
Get() (value interface{})
:获取当前协程已设置的变量值,若未设置则为nil
Set(v interface{}) interface{}
:设置当前协程的上下文变量值,返回之前已设置的旧值Del() (v interface{})
:删除当前协程的上下文变量值,返回已删除的旧值Clear()
:彻底清理此上下文变量在所有协程中保存的旧值
Get/Set/Del
的内部实现采用无锁设计,在大部分情况下,它的性能表现都应该非常稳定且高效。垃圾回收
routine
库内部维护了全局的storages
变量,它存储了全部协程的上下文变量信息,在读写时基于协程的goid
和协程变量的ptr
进行变量寻址映射。routine
内部分配了一个全局的GCTimer
,此定时器会在storages
需要被清理时启动,定时扫描并清理dead
协程在storages
中缓存的上下文变量,从而避免可能出现的内存泄露隐患。