一切皆有可能——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()返回了主協程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/Set/Del的內部實現採用無鎖設計,在大部分情況下,它的性能表現都應該非常穩定且高效。

垃圾回收

routine庫內部維護了全局的storages變量,它存儲了全部協程的上下文變量信息,在讀寫時基於協程的goid和協程變量的ptr進行變量尋址映射。

在進程的整個生命週期中,它可能會創建於銷燬無數個協程,那麼這些協程的上下文變量如何清理呢?

爲解決這個問題,routine內部分配了一個全局的GCTimer,此定時器會在storages需要被清理時啓動,定時掃描並清理dead協程在storages中緩存的上下文變量,從而避免可能出現的內存泄露隱患。

License

MIT

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/CPthQSqUIO-u-VkTQSMVXA