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

CodeNebula
• 阅读 8925

开源仓库: 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:  <nil>
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

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Wesley13 Wesley13
3年前
FLV文件格式
1.        FLV文件对齐方式FLV文件以大端对齐方式存放多字节整型。如存放数字无符号16位的数字300(0x012C),那么在FLV文件中存放的顺序是:|0x01|0x2C|。如果是无符号32位数字300(0x0000012C),那么在FLV文件中的存放顺序是:|0x00|0x00|0x00|0x01|0x2C。2.  
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
PHP创建多级树型结构
<!lang:php<?php$areaarray(array('id'1,'pid'0,'name''中国'),array('id'5,'pid'0,'name''美国'),array('id'2,'pid'1,'name''吉林'),array('id'4,'pid'2,'n
Wesley13 Wesley13
3年前
Java日期时间API系列36
  十二时辰,古代劳动人民把一昼夜划分成十二个时段,每一个时段叫一个时辰。二十四小时和十二时辰对照表:时辰时间24时制子时深夜11:00凌晨01:0023:0001:00丑时上午01:00上午03:0001:0003:00寅时上午03:00上午0
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
1年前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这
美凌格栋栋酱 美凌格栋栋酱
4个月前
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(