Go 语言并发编程的 “工具箱”

📅 发布时间:2026/7/12 12:12:27 👁️ 浏览次数:
Go 语言并发编程的 “工具箱”
Go 语言并发编程的 “工具箱”sync包是 Go 语言并发编程的 “工具箱”里面的每一个 API 都是为了解决特定的并发问题设计的。基础协作WaitGroup等待组作用等待一组 goroutine 全部执行完毕是最常用的 “并发等待” 工具。原理内部是一个计数器Add(n)增加计数Done()减少计数Wait()阻塞直到计数归零。packagemainimport(fmtsynctime)funcmain(){varwg sync.WaitGroup// 启动 3 个 goroutinefori:1;i3;i{wg.Add(1)// 启动前加 1gofunc(idint){deferwg.Done()// goroutine 结束前减 1fmt.Printf(Goroutine %d 开始工作\n,id)time.Sleep(1*time.Second)fmt.Printf(Goroutine %d 工作完成\n,id)}(i)}fmt.Println(主 goroutine 等待所有子 goroutine 完成...)wg.Wait()// 阻塞直到计数归零fmt.Println(所有工作完成)}适用场景并发批量处理任务如并发下载 10 个文件、并发查询 5 个数据库主 goroutine 需要等所有子 goroutine 干完活再继续执行。互斥锁Mutex互斥锁作用保证同一时间只有一个 goroutine访问共享资源防止 “竞态条件”。原理Lock()加锁Unlock()解锁加锁后其他 goroutine 必须等解锁后才能加锁。packagemainimport(fmtsync)var(countintmu sync.Mutex)funcincrement(){mu.Lock()// 加锁锁住共享资源 countdefermu.Unlock()// 函数结束前解锁count}funcmain(){varwg sync.WaitGroupfori:0;i1000;i{wg.Add(1)gofunc(){deferwg.Done()increment()}()}wg.Wait()fmt.Println(最终 count 值,count)// 不加锁会小于 1000加锁后一定是 1000}适用场景多个 goroutine 同时修改同一个共享变量如全局计数器、共享缓存任何需要 “独占访问” 共享资源的场景。读写锁RWMutex读写互斥锁作用Mutex的升级版适用于读多写少的场景 —— 读操作可以并发写操作必须独占。原理读锁RLock()/RUnlock()多个 goroutine 可以同时加读锁读与读不互斥写锁Lock()/Unlock()写锁与读锁、写锁与写锁都互斥写操作时不能读也不能写。packagemainimport(fmtsynctime)var(datamap[string]stringrwMu sync.RWMutex)// 读操作加读锁多个 goroutine 可以并发读funcreadData(keystring){rwMu.RLock()// 加读锁deferrwMu.RUnlock()// 解读锁fmt.Printf(读取 %s: %s\n,key,data[key])time.Sleep(100*time.Millisecond)// 模拟读耗时}// 写操作加写锁独占访问funcwriteData(key,valuestring){rwMu.Lock()// 加写锁deferrwMu.Unlock()// 解写锁fmt.Printf(写入 %s: %s\n,key,value)data[key]value time.Sleep(500*time.Millisecond)// 模拟写耗时}funcmain(){datamake(map[string]string)varwg sync.WaitGroup// 启动 5 个读 goroutine可以并发fori:1;i5;i{wg.Add(1)gofunc(idint){deferwg.Done()readData(fmt.Sprintf(key%d,id%2))// 调度 0 与 1}(i)}// 启动 1 个写 goroutine会阻塞所有读wg.Add(1)gofunc(){deferwg.Done()writeData(key1,value1)}()wg.Wait()/* 写入 key1: value1 读取 key1: value1 读取 key0: 读取 key0: 读取 key1: value1 读取 key1: value1 写 goroutine 抢到了锁先执行了写入 ( goroutine 启动顺序 不保证先读后写。) 写锁释放后所有读 goroutine 才开始执行 读 goroutine 读取 key1 时已经被写 goroutine 写成 value1 读 goroutine 读取 key0 时map 中没有这个 key所以打印空字符串 */}适用场景读多写少的共享资源如配置文件、缓存数据需要区分 “读并发” 和 “写独占” 的场景比Mutex性能更好。单次执行Once作用保证某个函数只执行一次即使在并发场景下也如此。原理内部用Mutex 原子变量实现Do(f)只会调用f一次。packagemainimport(fmtsync)typeConfigstruct{Envstring}var(config*Config once sync.Once)// 初始化配置只会执行一次funcinitConfig(){fmt.Println(初始化配置只会执行一次)configConfig{Env:dev}}// 获取配置并发安全的单例funcGetConfig()*Config{once.Do(initConfig)// 保证 initConfig 只执行一次returnconfig}funcmain(){varwg sync.WaitGroupfori:0;i5;i{wg.Add(1)gofunc(){deferwg.Done()cfg:GetConfig()fmt.Printf(获取到配置%v\n,cfg)}()}wg.Wait()/* 初始化配置只会执行一次 获取到配置{dev} 获取到配置{dev} 获取到配置{dev} 获取到配置{dev} 获取到配置{dev} */}适用场景单例模式如全局配置、数据库连接池初始化只需要执行一次的初始化操作如加载配置文件、注册服务。并发 MapMap作用并发安全的 Map普通 Map 并发读写会 panicsync.Map不需要加锁就能并发读写。核心 APIStore(key, value)存键值对Load(key)取键值对Delete(key)删键值对Range(f func(key, value interface{}) bool)遍历所有键值对。packagemainimport(fmtsync)funcmain(){varm sync.Map// 1. 并发写入varwg sync.WaitGroupfori:1;i5;i{wg.Add(1)gofunc(idint){deferwg.Done()key:fmt.Sprintf(key%d,id)m.Store(key,fmt.Sprintf(value%d,id))fmt.Printf(写入 %s\n,key)}(i)}wg.Wait()// 2. 读取单个值ifval,ok:m.Load(key3);ok{fmt.Printf(读取 key3: %s\n,val)}// 3. 遍历所有键值对fmt.Println(遍历所有键值对)m.Range(func(key,valueinterface{})bool{fmt.Printf(%s: %s\n,key,value)returntrue// 返回 true 继续遍历false 停止})}适用场景并发缓存如热点数据缓存、Session 存储多个 goroutine 同时读写的 Map避免自己加锁的麻烦注意sync.Map适用于 “读多写少” 且 “键值对相对稳定” 的场景写多读少的场景性能不如 “普通 Map Mutex”。对象池Pool作用复用对象减少内存分配和 GC垃圾回收压力提升性能。原理内部是一个 “池子”Get()从池子里取对象Put()把用完的对象放回池子如果池子空了会调用New函数创建新对象。packagemainimport(fmtsync)// 模拟一个需要频繁创建的对象比如缓冲区typeBufferstruct{data[]byte}funcmain(){// 1. 定义对象池pool:sync.Pool{New:func()interface{}{fmt.Println(创建新的 Buffer 对象)returnBuffer{data:make([]byte,1024)}},}// 2. 第一次获取池子空了调用 New 创建buf1:pool.Get().(*Buffer)// 判断 buf1 是否为空ifbuf1nil||buf1.datanil{fmt.Println(buf1 是空对象)}else{fmt.Println(buf1 是有效对象)}// buf1 是有效对象// 3. 用完放回池子pool.Put(buf1)fmt.Println(buf1 放回池子)// 4. 第二次获取直接从池子取不创建新对象buf2:pool.Get().(*Buffer)// 判断 buf2 是否与 buf1 是同一个对象ifbuf1buf2{fmt.Println(buf2 与 buf1 是同一个对象)}else{fmt.Println(buf2 与 buf1 不是同一个对象)}// buf2 与 buf1 是同一个对象}适用场景:频繁创建和销毁的大对象如数据库连接、HTTP 连接、大缓冲区需要减少 GC 压力的高性能场景如高并发 Web 服务、消息队列。复用大对象最常用bytes.Buffer、[]byte切片、大型的自定义Struct。解析一个巨大的 JSON 时你需要一个临时缓存区。用完还给Pool下一个请求接着用。数据库连接的辅助注意不是连接池本身虽然它不能当数据库连接池用但可以用来存放用于构建查询语句的临时字符串缓冲。Web 框架上下文比如Gin 框架。每一个 HTTP 请求进来Gin 都会创建一个Context对象。Gin 并没有每次都new一个而是从sync.Pool里取。请求结束放回池子。条件变量Cond作用协调多个 goroutine 之间的 “等待 - 通知” 逻辑比 channel 更灵活比如可以一次性通知所有等待的 goroutine。原理Wait()释放锁并阻塞等待被通知后重新加锁Signal()通知一个等待的 goroutineBroadcast()通知所有等待的 goroutine必须和Mutex一起用。// 生产者 - 消费者模式packagemainimport(fmtsynctime)funcmain(){varmu sync.Mutex cond:sync.NewCond(mu)// 用 Mutex 创建 Condvarqueue[]int// 共享队列// 生产者往队列里放数据producer:func(){fori:1;i3;i{mu.Lock()queueappend(queue,i)fmt.Printf(生产了 %d队列长度%d\n,i,len(queue))cond.Signal()// 通知一个消费者mu.Unlock()time.Sleep(500*time.Millisecond)}}// 消费者从队列里取数据consumer:func(idint){for{mu.Lock()// 队列空了就等待forlen(queue)0{cond.Wait()// 释放锁并阻塞被通知后重新加锁}// 取数据item:queue[0]queuequeue[1:]fmt.Printf(消费者 %d 消费了 %d队列长度%d\n,id,item,len(queue))mu.Unlock()time.Sleep(1*time.Second)}}// 启动 1 个生产者2 个消费者goproducer()goconsumer(1)goconsumer(2)// 简单等待time.Sleep(5*time.Second)}适用场景:复杂的生产者 - 消费者模式需要灵活控制通知逻辑需要 “一次性通知所有等待 goroutine” 的场景如配置更新后通知所有 worker 重新加载Cond比较复杂能用 channel 解决的场景优先用 channelchannel 解决不了再用Cond。总结API核心作用一句话适用场景WaitGroup等待一组 goroutine 完成并发批量处理任务等所有任务干完再继续Mutex互斥锁独占访问共享资源多个 goroutine 同时修改同一个变量RWMutex读写锁读并发、写独占读多写少的共享资源如配置、缓存Once保证函数只执行一次单例模式、全局初始化Map并发安全的 Map多个 goroutine 同时读写的缓存Pool对象池复用对象频繁创建销毁的大对象减少 GCCond条件变量协调等待 - 通知复杂的生产者 - 消费者、需要广播通知的场景选择原则优先用简单的WaitGroupMutexRWMutexMapPoolCond读多写少优先考虑RWMutex锁或Map并发 Map性能优化频繁创建大对象时用Pool复杂协调channel 解决不了的场景再用Cond。