ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Buildah 依赖剖析:modern-go/concurrent 的并发 Map 与可取消 Goroutine Executor

Buildah 依赖剖析:modern-go/concurrent 的并发 Map 与可取消 Goroutine Executor 云原生【免费下载链接】buildahA tool that facilitates building OCI images.项目地址https://gitcode.com/gh_mirrors/bu/buildah点击查看免费下载本文以 Buildah 仓库中 vendor 目录下的 modern-go/concurrent 库文档为主体完整讲解该库提供的两个核心组件——concurrent.Map跨 Go 版本的并发 Map 封装与concurrent.Executor具有明确所有权、可取消的 goroutine 执行器的用法与设计原理并结合仓库内源码逐行剖析其实现细节与真实调用场景帮助读者理解这类底层并发工具在 JSON 序列化缓存等场景中的作用。concurrent.Map让 sync.Map 在任意 Go 版本可用concurrent库的第一个组件是concurrent.MapREADME 给出的最简用法如下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.MapLoad、Store、LoadOrStore、Delete等全部方法都自动获得且直接享受sync.Map针对读多写少场景的无锁优化read/_dirty 双 map 结构。当前仓库使用的 Go 工具链版本远高于 1.9因此实际生效的就是这份封装。go_below_19.go带有//build !go1.9标签仅在旧工具链下参与编译。它用sync.RWMutexmap[interface{}]interface{}手工实现了线程安全 Maptype 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可以通过回调处理 panicgoroutine 内的 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 与 InfoLoggerpanic 与等待日志分别写到两个可替换的 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默认输出到 stderrInfoLogger默认丢弃写入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.Mapgo_above_19.go与RWMutex mapgo_below_19.go之间切换对外 API 不变是库依赖中处理标准库版本差异的干净范例concurrent.Executor具体实现为UnboundedExecutor见 unbounded_executor.go把 goroutine 的取消与 panic 处理从野生变为有主按启动位置注册计数、context 协作式取消、recover 可替换回调、三级 Stop 语义配套可替换的 ErrorLogger / InfoLogger。理解这套实现有助于阅读 Buildah 依赖链上所有基于 json-iterator 的 JSON 序列化代码它们的性能与并发正确性部分就建立在这些缓存容器和执行器的设计之上。赞分享云原生【免费下载链接】buildahA tool that facilitates building OCI images.项目地址https://gitcode.com/gh_mirrors/bu/buildah点击查看免费下载相关推荐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 执行器 本篇技术指南聚云原生集群管理虚拟化多集群上一篇Airbyte source-gong 连接器深度解析增量同步、错误处理与 Fivetran 兼容性设计下一篇Node.js模块样板社区贡献指南如何扩展和定制你的样板模板创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表