一切皆有可能——Golang中的”ThreadLocal“库

开源仓库: https://github.com/go-eden/ro...

本文介绍的是新写的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()返回了主协程1AllGoids()返回了主协程及协程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:  
name3:  hello world
name2:  hello world

API文档

此章节详细介绍了routine库封装的全部接口,以及它们的核心功能、实现方式等。

Goid() (id int64)

获取当前goroutinegoid

在正常情况下,Goid()优先尝试通过go_tls的方式直接获取,此操作性能极高,耗时通常只相当于rand.Int()的五分之一。

若出现版本不兼容等错误时,Goid()会尝试降级,即从runtime.Stack信息中解析获取,此时性能会急剧下降约千倍,但它可以保证功能正常可用。

AllGoids() (ids []int64)

获取当前进程全部活跃goroutinegoid

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中缓存的上下文变量,从而避免可能出现的内存泄露隐患。

License

MIT

你可能感兴趣的:(一切皆有可能——Golang中的”ThreadLocal“库)