- 云原生
【免费下载链接】buildah
A tool that facilitates building OCI images.
本文以 Buildah 仓库中 vendor 目录下的 modern-go/concurrent 库文档为主体,完整讲解该库提供的两个核心组件——concurrent.Map(跨 Go 版本的并发 Map 封装)与concurrent.Executor(具有明确所有权、可取消的 goroutine 执行器)的用法与设计原理,并结合仓库内源码逐行剖析其实现细节与真实调用场景,帮助读者理解这类底层并发工具在 JSON 序列化缓存等场景中的作用。
concurrent.Map:让 sync.Map 在任意 Go 版本可用
concurrent库的第一个组件是concurrent.Map,README 给出的最简用法如下:
m := concurrent.NewMap() m.Store("hello", "world") elem, found := m.Load("hello") // elem will be "world" // found will be true它解决的是一个可移植性问题:标准库的sync.Map从 Go 1.9 才引入。若项目需要兼容更早的 Go 版本,直接使用sync.Map会编译失败;而concurrent.Map提供与sync.Map一致的NewMap/Load/StoreAPI,使业务代码无需关心底层差异。
从源码结构看,这一可移植性是靠两份带 build tag 的实现文件实现的:
go_above_19.go:文件头带有
//+build go1.9标签。在 Go 1.9 及以上工具链下编译时,Map直接内嵌标准库的sync.Map:// Map is a wrapper for sync.Map introduced in go1.9 type Map struct { sync.Map } // NewMap creates a thread safe Map func NewMap() *Map { return &Map{} }由于内嵌
sync.Map,Load、Store、LoadOrStore、Delete等全部方法都自动获得,且直接享受sync.Map针对"读多写少"场景的无锁优化(read/_dirty 双 map 结构)。当前仓库使用的 Go 工具链版本远高于 1.9,因此实际生效的就是这份封装。go_below_19.go:带有
//+build !go1.9标签,仅在旧工具链下参与编译。它用sync.RWMutex+map[interface{}]interface{}手工实现了线程安全 Map:type Map struct { lock sync.RWMutex data map[interface{}]interface{} } func (m *Map) Load(key interface{}) (elem interface{}, found bool) { m.lock.RLock() elem, found = m.data[key] m.lock.RUnlock() return } func (m *Map) Store(key interface{}, elem interface{}) { m.lock.Lock() m.data[key] = elem m.lock.Unlock() }读操作持
RLock允许多读并发,写操作持独占锁,初始容量固定为 32。两份文件对外暴露完全相同的NewMap()入口,上层调用方对版本差异无感知——这正是"backport"(功能回移封装)模式的典型写法。
concurrent.Executor:把 goroutine 的生命周期交给执行器
README 的第二个组件是concurrent.Executor。原生go语句启动的 goroutine 一旦派发出去,调用方就失去了对它的句柄:无法统一取消,任何一个 goroutine 的 panic 还会直接崩溃整个进程。concurrent.Executor通过"goroutine 显式归属于执行器"的设计解决这两个问题:
executor := concurrent.NewUnboundedExecutor() executor.Go(func(ctx context.Context) { everyMillisecond := time.NewTicker(time.Millisecond) for { select { case <-ctx.Done(): fmt.Println("goroutine exited") return case <-everyMillisecond.C: // do something } } }) time.Sleep(time.Second) executor.StopAndWaitForever() fmt.Println("executor stopped")README 明确给出了两个核心收益:
- 可以通过
Stop/StopAndWait/StopAndWaitForever停止执行器,从而取消它名下所有 goroutine; - 可以通过回调处理 panic,goroutine 内的 panic 默认不再导致应用崩溃。
Executor 接口与 UnboundedExecutor 的具体实现
executor.go 定义了最小接口:
type Executor interface { // Go starts a new goroutine controlled by the context Go(handler func(ctx context.Context)) }值得注意的是接口的刻意取舍:它只暴露Go,不提供Stop。源码注释解释了原因——启动并持有执行器的一方才有权停止它,因此需要停止操作时应使用具体类型*UnboundedExecutor,而不是这个接口。这是一个"权限最小化"的 API 设计:把取消权收敛到持有者手中。
unbounded_executor.go 是具体实现。UnboundedExecutor内部由一个可取消的 context 驱动:
type UnboundedExecutor struct { ctx context.Context cancel context.CancelFunc activeGoroutinesMutex *sync.Mutex activeGoroutines map[string]int HandlePanic func(recovered interface{}, funcName string) }几个关键机制值得展开:
1. goroutine 注册与计数。Go方法启动 goroutine 前,会先用reflect.ValueOf(handler).Pointer()与runtime.FuncForPC拿到 handler 函数名及定义处的file:line,并以此作为 key 在activeGoroutines中计数:
func (executor *UnboundedExecutor) Go(handler func(ctx context.Context)) { pc := reflect.ValueOf(handler).Pointer() f := runtime.FuncForPC(pc) funcName := f.Name() file, line := f.FileLine(pc) executor.activeGoroutinesMutex.Lock() defer executor.activeGoroutinesMutex.Unlock() startFrom := fmt.Sprintf("%s:%d", file, line) executor.activeGoroutines[startFrom] += 1 go func() { defer func() { recovered := recover() // if you want to quit a goroutine without trigger HandlePanic // use runtime.Goexit() to quit if recovered != nil { if executor.HandlePanic == nil { HandlePanic(recovered, funcName) } else { executor.HandlePanic(recovered, funcName) } } executor.activeGoroutinesMutex.Lock() executor.activeGoroutines[startFrom] -= 1 executor.activeGoroutinesMutex.Unlock() }() handler(executor.ctx) }() }按启动位置(而非每个 goroutine 实例)聚合计数,使得"还有哪些位置派发的 goroutine 没有退出"这类信息在等待退出时可以直接用于诊断(见下文checkNoActiveGoroutines)。
2. panic 恢复与可替换回调。每个被包装的 goroutine 都带有recover():发生 panic 时优先调用实例级executor.HandlePanic,未设置时回退到包级默认回调HandlePanic,其默认行为是把 panic 值与完整堆栈打印到ErrorLogger,而不是让进程崩溃。若希望 goroutine 静默退出而不触发 panic 处理,源码注释建议显式调用runtime.Goexit()。
3. 三级停止语义。
// Stop cancel all goroutines started by this executor without wait func (executor *UnboundedExecutor) Stop() { executor.cancel() } func (executor *UnboundedExecutor) StopAndWaitForever() { executor.StopAndWait(context.Background()) } func (executor *UnboundedExecutor) StopAndWait(ctx context.Context) { executor.cancel() for { oneHundredMilliseconds := time.NewTimer(time.Millisecond * 100) select { case <-oneHundredMilliseconds.C: if executor.checkNoActiveGoroutines() { return } case <-ctx.Done(): return } } }Stop只调用cancel(),向所有通过executor.ctx派发的 goroutine 广播取消信号,但不等待它们退出(协作式取消,goroutine 必须在select中监听ctx.Done()才会响应);StopAndWait在取消之后每 100ms 轮询一次activeGoroutines,全部归零才返回;轮询可通过传入的 ctx 中途放弃;StopAndWaitForever是StopAndWait(context.Background())的便捷形式,等待永不超时。
等待期间checkNoActiveGoroutines会把仍存活的 goroutine 及其启动位置、数量通过InfoLogger输出,便于定位"谁没有响应取消"。
4. 全局执行器。库还提供了一个包级变量:
// GlobalUnboundedExecutor has the life cycle of the program itself var GlobalUnboundedExecutor = NewUnboundedExecutor()它的生命周期与程序相同,适合承载"main 退出前需要统一关停"的常驻 goroutine;源码注释也强调它"不会魔法般地知道 main 函数退出",需要 main 显式调用 Stop。
日志出口:ErrorLogger 与 InfoLogger
panic 与等待日志分别写到两个可替换的 logger 上,定义在 log.go:
// ErrorLogger is used to print out error, can be set to writer other than stderr var ErrorLogger = log.New(os.Stderr, "", 0) // InfoLogger is used to print informational message, default to off var InfoLogger = log.New(ioutil.Discard, "", 0)ErrorLogger默认输出到 stderr;InfoLogger默认丢弃(写入ioutil.Discard),即等待日志默认静默,业务方可以按需替换为真实 writer。两者都是可写变量,允许集成方接入自己的日志系统。
在 Buildah 仓库中的真实使用:json-iterator 的编码器缓存
concurrent.Map并非孤立存在——它在当前仓库中最直接的下游消费者是 vendor 中的 json-iterator/go 库。其冻结配置(frozenConfig)为每个配置维护 decoder/encoder 两级缓存:
type frozenConfig struct { ... decoderCache *concurrent.Map encoderCache *concurrent.Map ... } func (cfg *frozenConfig) initCache() { cfg.decoderCache = concurrent.NewMap() cfg.encoderCache = concurrent.NewMap() }缓存的读写路径如addDecoderToCache/getDecoderFromCache所示,均为典型的"多线程高频读、低频写"模式:
func (cfg *frozenConfig) getDecoderFromCache(cacheKey uintptr) ValDecoder { decoder, found := cfg.decoderCache.Load(cacheKey) if found { return decoder.(ValDecoder) } return nil }此外还有包级共享的var cfgCache = concurrent.NewMap(),用于按Config值复用已 Froze 的配置对象。这类缓存如果误用普通map加sync.Mutex简单保护,在高并发 JSON 编解码路径上会成为明显的锁竞争点;而 Go 1.9+ 下concurrent.Map底层就是sync.Map,读命中时基本无锁——这正是 json-iterator 选择它来承载热路径缓存的原因。
小结
回到 README 的两句话概括:
concurrent.Map是sync.Map的跨版本 backport,用 build tag 在"内嵌sync.Map"(go_above_19.go)与"RWMutex + map"(go_below_19.go)之间切换,对外 API 不变,是库依赖中处理标准库版本差异的干净范例;concurrent.Executor(具体实现为UnboundedExecutor,见 unbounded_executor.go)把 goroutine 的取消与 panic 处理从"野生"变为"有主":按启动位置注册计数、context 协作式取消、recover + 可替换回调、三级 Stop 语义,配套可替换的 ErrorLogger / InfoLogger。
理解这套实现有助于阅读 Buildah 依赖链上所有基于 json-iterator 的 JSON 序列化代码:它们的性能与并发正确性,部分就建立在这些缓存容器和执行器的设计之上。
- 云原生
【免费下载链接】buildah
A tool that facilitates building OCI images.
相关推荐
KubeSphere 依赖解析:modern-go/concurrent 的并发 Map 与可取消 Goroutine Executor 实战指南
KubeSphere 依赖解析:modern go/concurrent 的并发 Map 与可取消 Goroutine Executor 实战指南 导读 git
后端云原生容器编排微服务OpenCloud 依赖解析:modern-go/concurrent 并发 Map 与可取消 Goroutine 执行器实战
OpenCloud 依赖解析:modern go/concurrent 并发 Map 与可取消 Goroutine 执行器实战 导读 本文以 OpenCloud
后端微服务存储认证鉴权vcluster 中 vendored 的 modern-go/concurrent 库解析:并发 Map 与可取消 Goroutine 执行器
vcluster 中 vendored 的 modern go/concurrent 库解析:并发 Map 与可取消 Goroutine 执行器 本篇技术指南聚
云原生集群管理虚拟化多集群
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考