背景

由于官方不支持 ThreadLocal,在业务中传参经常需要传递 context,造成参数混乱,开发效率低下,跨方法实现传参变得困难。

需要解决的核心问题:

1、 数据存储,g.labels unsafe.Pointer 字段在业务场景下不会被使用,所以我们可以把数据的指针存储在该字段上。

2、g.labels字段存在一个问题就是在创建子协程的时候指针会被自动复制,而数据不会。所以我们需要在访问该字段时,断该字段的内容是否为当前协程创建的,如果不是则清空。所以这就要求我们需要首先获取 goid,goid 的获取下文会进行说明。

3、垃圾回收,由于数据指针存储在协程结构 g.labels 字段,该字段在协程结束后自动被置为 nil,所以在下次垃圾回收时只要关联的数据没有被额外的引用便会自动回收。

架构

ThreadLocal for Golang-LMLPHP

Goid

github 上有很多库已经实现了不同平台的汇编调用 go runtime 库中的 getg()。

然后通过 unsafe 便宜一定的地址空间获取 goid, 该方法与直接调用耗时相当,性能非常高。

在不同的平台上(windows、linux、mac)均能正常获取到。

在极特殊的情况下(目前未发现),如果获取不到会通过 runtime.Stack(false) 方式获取并打印警告日志,此时性能会降低,但能保证不出错。

FastThreadLocal

此设计参考了 netty 中的 FastThreadLocal。

将 threadLocalMap 数据结构由 Map 改为 Slice,在创建 ThreadLocal 实例的时候内部维护一个 id,该 id 为全局自增。

ThreadLocal 实例的 id 字段作为 Slice 的下标,这样在 Get,Set 操作时由原来的操作 Map 变更为通过下标操作 Slice,当 ThreadLocal 实例多的时候性能提升明显。

InheritableThreadLocal 实现

参考 java,在创建协程的时候先复制当前协程的 threadLocalMap,当任务真正的在子协程执行的时候,将复制出来的 threadLocalMap 赋值到当前协程,便可实现跨协程继承数据。

注意,如果是非指针的值数据继承过去的是复制后的值,而指针类型的数据跨协程继承的数据复制的是指针的值。

即在另一个协程对结构内部的字段内容进行修改实际上会影响两个协程。如果要避免这种情况的出现,需要实现 Cloneable 接口。此时复制的时候会将指针指向的内容进行复制一份。

垃圾回收

由于存储数据指针的g.labels字段会被自动清空,所以不需要额外的垃圾回收。

安装

使用

以下代码简单演示了ThreadLocal的创建、设置、获取、跨协程传播等:

package main

import (
	"fmt"
	"github.com/timandy/routine"
	"time"
)

var threadLocal = routine.NewThreadLocal()
var inheritableThreadLocal = routine.NewInheritableThreadLocal()

func main() {
	threadLocal.Set("hello world")
	inheritableThreadLocal.Set("Hello world2")
	fmt.Println("threadLocal:", threadLocal.Get())
	fmt.Println("inheritableThreadLocal:", inheritableThreadLocal.Get())

	// 其他协程无法读取之前赋值的“hello world”。
	go func() {
		fmt.Println("threadLocal in goroutine:", threadLocal.Get())
		fmt.Println("inheritableThreadLocal in goroutine:", inheritableThreadLocal.Get())
	}()

	// 但是,可以通过 Go 函数启动一个新的 goroutine。当前主 goroutine 的所有可继承变量都可以自动传递。
	routine.Go(func() {
		fmt.Println("threadLocal in goroutine by Go:", threadLocal.Get())
		fmt.Println("inheritableThreadLocal in goroutine by Go:", inheritableThreadLocal.Get())
	})

	time.Sleep(time.Second)
}

执行结果为:

threadLocal: hello world
inheritableThreadLocal: Hello world2
threadLocal in goroutine: <nil>
inheritableThreadLocal in goroutine: <nil>
threadLocal in goroutine by Go: <nil>
inheritableThreadLocal in goroutine by Go: Hello world2

支持网格

✅:支持

源码

https://github.com/timandy/routine

08-26 17:18