• ThreadLocal for Golang


    背景

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

    需要解决的核心问题:

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

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

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

    架构

    架构图

    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字段会被自动清空,所以不需要额外的垃圾回收。

    安装

    go get github.com/timandy/routine

    使用

    以下代码简单演示了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: 
    inheritableThreadLocal in goroutine: 
    threadLocal in goroutine by Go: 
    inheritableThreadLocal in goroutine by Go: Hello world2
    

    支持网格

    darwin linux windows freebsd
    386 386
    amd64 amd64
    armv6 armv6
    armv7 armv7
    arm64 arm64
    ppc64 ppc64
    s390x s390x
    darwin linux windows freebsd

    ✅:支持

    源码

    https://github.com/timandy/routine

  • 相关阅读:
    大数据资产管理架构设计篇-来自《数据资产管理核心技术与应用》一书的权威讲解
    Wireshark 如何查找包含特定数据的数据帧
    Allegro Design Entry HDL(OrCAD Capture HDL)RF-PCB菜单详细介绍
    关于 java 的动态绑定机制
    Squid代理服务器应用
    《lwip学习6》-- ARP协议
    UE5 c++将自定义UserWdiget添加到对应菜单栏
    Linux——进程间通信——system V系列
    IDEA自定义注释模版
    Unity SRP 管线【第一讲:自定义渲染管线】
  • 原文地址:https://www.cnblogs.com/xuchonglei/p/16627590.html