ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Go WaitGroup与Once——并发同步的基石

Go WaitGroup与Once——并发同步的基石 1. 引言在 Go 并发编程中sync.WaitGroup和sync.Once是两个最基础也最常用的同步原语。WaitGroup用于等待一组 goroutine 完成任务而Once则确保某个函数只执行一次。理解它们的原理与易错点是写出健壮并发代码的前提。2. 核心知识点2.1 sync.WaitGroup 三方法WaitGroup提供三个方法Add(delta int)增加或减少等待的计数器Done()等价于Add(-1)表示一个任务完成Wait()阻塞直到计数器归零varwg sync.WaitGroup wg.Add(1)gofunc(){deferwg.Done()// 执行任务}()wg.Wait()2.2 Add 必须在 Wait 前调用Add必须在Wait之前调用否则可能导致 panic 或Wait提前返回。这是因为Wait在计数器为 0 时会立即返回如果在 goroutine 内部才调用AddWait可能已经返回了。2.3 WaitGroup 不可复制WaitGroup内部包含计数器状态复制后行为异常。因此WaitGroup应当通过指针传递绝不能复制。// 错误复制 WaitGroupfuncprocess(wg sync.WaitGroup){// 危险deferwg.Done()}// 正确传递指针funcprocess(wg*sync.WaitGroup){deferwg.Done()}2.4 sync.OnceDo(f) 确保 f 只执行一次sync.Once的Do(f)方法确保函数f只执行一次即使被多个 goroutine 并发调用。varonce sync.Oncevarconfig*ConfigfuncgetConfig()*Config{once.Do(func(){configloadConfig()// 只执行一次})returnconfig}2.5 Once 的实现atomic Mutex 双重检查sync.Once内部使用atomic和Mutex实现双重检查确保高效且线程安全typeOncestruct{doneuint32m Mutex}func(o*Once)Do(ffunc()){ifatomic.LoadUint32(o.done)0{o.doSlow(f)}}func(o*Once)doSlow(ffunc()){o.m.Lock()defero.m.Unlock()ifo.done0{deferatomic.StoreUint32(o.done,1)f()}}2.6 sync.OnceFunc / sync.OnceValueGo 1.21Go 1.21 引入了更便捷的封装sync.OnceFunc(f)返回一个只执行一次的包装函数sync.OnceValue(f)返回一个只执行一次并缓存返回值的函数getConfig:sync.OnceValue(loadConfig)cfg:getConfig()// 首次调用执行 loadConfigcfg2:getConfig()// 返回缓存3. WaitGroup 使用模式3.1 主 goroutine Add子 goroutine Done最标准的模式主 goroutine 负责Add子 goroutine 负责Done。// 正确主 goroutine Addvarwg sync.WaitGroupfori:0;i10;i{wg.Add(1)gofunc(nint){deferwg.Done()process(n)}(i)}wg.Wait()3.2 子 goroutine 内 Add 需谨慎在 goroutine 内部调用Add是危险的可能导致Wait提前返回// 错误goroutine 内 Addfori:0;i10;i{gofunc(nint){wg.Add(1)// 危险Wait 可能已返回deferwg.Done()process(n)}(i)}wg.Wait()3.3 配合 channel 实现优雅关闭WaitGroup常与channel配合实现优雅关闭varwg sync.WaitGroup jobs:make(chanint,10)// 消费者fori:0;i3;i{wg.Add(1)gofunc(){deferwg.Done()forjob:rangejobs{process(job)}}()}// 生产者fori:0;i10;i{jobs-i}close(jobs)wg.Wait()// 等待所有消费者完成4. 易错点与常见误解4.1 在 goroutine 内调用 Add如上所述这可能导致Wait提前返回因为Wait可能在Add执行前就检查到计数器为 0。4.2 复制 WaitGroup复制WaitGroup会复制其内部状态导致计数器不一致行为异常。务必使用指针传递。4.3 Done 调用次数多于 Add → panic如果Done的调用次数超过Add的总数计数器会变为负数触发 panicvarwg sync.WaitGroup wg.Add(1)gofunc(){wg.Done()wg.Done()// panic: negative WaitGroup counter}()wg.Wait()4.4 Once.Do 中的函数 panic 后Once 仍视为已执行如果Once.Do中的函数 panicOnce仍会标记为已执行不会重试varonce sync.Once once.Do(func(){panic(boom)// panic 后 Once 仍标记为已执行})once.Do(func(){fmt.Println(不会执行)})5. 完整示例5.1 WaitGroup 批量任务packagemainimport(fmtsynctime)funcprocess(nint){time.Sleep(time.Millisecond*100)fmt.Printf(处理任务 %d\n,n)}funcmain(){varwg sync.WaitGroupfori:0;i10;i{wg.Add(1)gofunc(nint){deferwg.Done()process(n)}(i)}wg.Wait()fmt.Println(所有任务完成)}5.2 Once 幂等初始化packagemainimport(fmtsync)var(once sync.Once configmap[string]string)funcloadConfig()map[string]string{fmt.Println(加载配置...)returnmap[string]string{host:localhost,port:8080}}funcgetConfig()map[string]string{once.Do(func(){configloadConfig()})returnconfig}funcmain(){// 并发调用但 loadConfig 只执行一次varwg sync.WaitGroupfori:0;i5;i{wg.Add(1)gofunc(){deferwg.Done()fmt.Println(getConfig())}()}wg.Wait()}5.3 OnceValue 简化写法Go 1.21packagemainimport(fmtsync)funcloadConfig()map[string]string{fmt.Println(加载配置...)returnmap[string]string{host:localhost,port:8080}}funcmain(){getConfig:sync.OnceValue(loadConfig)cfg1:getConfig()// 首次调用执行 loadConfigcfg2:getConfig()// 返回缓存fmt.Println(cfg1)fmt.Println(cfg2)}6. 总结WaitGroup用于等待一组 goroutine 完成核心是Add、Done、Wait三方法Add必须在Wait前调用且应在主 goroutine 中调用WaitGroup不可复制应通过指针传递Once确保函数只执行一次内部使用 atomic Mutex 双重检查Once.Do中的函数 panic 后不会重试Go 1.21 提供了OnceFunc和OnceValue便捷封装掌握这些同步原语能帮助你写出更健壮、更高效的并发代码。
返回列表