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

0 阅读8分钟

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

sync 包是 Go 语言并发编程的 “工具箱”,里面的每一个 API 都是为了解决特定的并发问题设计的。

基础协作:WaitGroup(等待组)

作用:等待一组 goroutine 全部执行完毕,是最常用的 “并发等待” 工具。

原理:内部是一个计数器,Add(n) 增加计数,Done() 减少计数,Wait() 阻塞直到计数归零。

package main

import (
	"fmt"
	"sync"
	"time"
)

func main() {
	var wg sync.WaitGroup

	// 启动 3 个 goroutine
	for i := 1; i <= 3; i++ {
		wg.Add(1) // 启动前加 1
		go func(id int) {
			defer wg.Done() // goroutine 结束前减 1
			fmt.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 必须等解锁后才能加锁。

package main

import (
	"fmt"
	"sync"
)

var (
	count int
	mu    sync.Mutex
)

func increment() {
	mu.Lock()         // 加锁:锁住共享资源 count
	defer mu.Unlock() // 函数结束前解锁
	count++
}

func main() {
	var wg sync.WaitGroup
	for i := 0; i < 1000; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			increment()
		}()
	}
	wg.Wait()
	fmt.Println("最终 count 值:", count) // 不加锁会小于 1000,加锁后一定是 1000
}

适用场景

  • 多个 goroutine 同时修改同一个共享变量(如全局计数器、共享缓存);
  • 任何需要 “独占访问” 共享资源的场景。

读写锁:RWMutex(读写互斥锁)

作用Mutex 的升级版,适用于读多写少的场景 —— 读操作可以并发,写操作必须独占。

原理

  • 读锁(RLock()/RUnlock()):多个 goroutine 可以同时加读锁(读与读不互斥);
  • 写锁(Lock()/Unlock()):写锁与读锁、写锁与写锁都互斥(写操作时,不能读也不能写)。
package main

import (
	"fmt"
	"sync"
	"time"
)

var (
	data map[string]string
	rwMu sync.RWMutex
)

// 读操作:加读锁,多个 goroutine 可以并发读
func readData(key string) {
	rwMu.RLock()         // 加读锁
	defer rwMu.RUnlock() // 解读锁
	fmt.Printf("读取 %s: %s\n", key, data[key])
	time.Sleep(100 * time.Millisecond) // 模拟读耗时
}

// 写操作:加写锁,独占访问
func writeData(key, value string) {
	rwMu.Lock()         // 加写锁
	defer rwMu.Unlock() // 解写锁
	fmt.Printf("写入 %s: %s\n", key, value)
	data[key] = value
	time.Sleep(500 * time.Millisecond) // 模拟写耗时
}

func main() {
	data = make(map[string]string)
	var wg sync.WaitGroup

	// 启动 5 个读 goroutine(可以并发)
	for i := 1; i <= 5; i++ {
		wg.Add(1)
		go func(id int) {
			defer wg.Done()
			readData(fmt.Sprintf("key%d", id%2)) // 调度 0 与 1
		}(i)
	}

	// 启动 1 个写 goroutine(会阻塞所有读)
	wg.Add(1)
	go func() {
		defer wg.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 一次。

package main

import (
	"fmt"
	"sync"
)

type Config struct {
	Env string
}

var (
	config *Config
	once   sync.Once
)

// 初始化配置:只会执行一次
func initConfig() {
	fmt.Println("初始化配置(只会执行一次)")
	config = &Config{Env: "dev"}
}

// 获取配置:并发安全的单例
func GetConfig() *Config {
	once.Do(initConfig) // 保证 initConfig 只执行一次
	return config
}

func main() {
	var wg sync.WaitGroup

	for i := 0; i < 5; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			cfg := GetConfig()
			fmt.Printf("获取到配置:%v\n", cfg)
		}()
	}
	wg.Wait()
	/*
	   初始化配置(只会执行一次)
	   获取到配置:&{dev}
	   获取到配置:&{dev}
	   获取到配置:&{dev}
	   获取到配置:&{dev}
	   获取到配置:&{dev}
	*/
}

适用场景

  • 单例模式(如全局配置、数据库连接池初始化);
  • 只需要执行一次的初始化操作(如加载配置文件、注册服务)。

并发 Map:Map

作用:并发安全的 Map,普通 Map 并发读写会 panic,sync.Map 不需要加锁就能并发读写。

核心 API

  • Store(key, value):存键值对;
  • Load(key):取键值对;
  • Delete(key):删键值对;
  • Range(f func(key, value interface{}) bool):遍历所有键值对。
package main

import (
	"fmt"
	"sync"
)

func main() {
	var m sync.Map

	// 1. 并发写入
	var wg sync.WaitGroup
	for i := 1; i <= 5; i++ {
		wg.Add(1)
		go func(id int) {
			defer wg.Done()
			key := fmt.Sprintf("key%d", id)
			m.Store(key, fmt.Sprintf("value%d", id))
			fmt.Printf("写入 %s\n", key)
		}(i)
	}
	wg.Wait()

	// 2. 读取单个值
	if val, ok := m.Load("key3"); ok {
		fmt.Printf("读取 key3: %s\n", val)
	}

	// 3. 遍历所有键值对
	fmt.Println("遍历所有键值对:")
	m.Range(func(key, value interface{}) bool {
		fmt.Printf("%s: %s\n", key, value)
		return true // 返回 true 继续遍历,false 停止
	})
}

适用场景

  • 并发缓存(如热点数据缓存、Session 存储);
  • 多个 goroutine 同时读写的 Map(避免自己加锁的麻烦);
  • 注意sync.Map 适用于 “读多写少” 且 “键值对相对稳定” 的场景,写多读少的场景性能不如 “普通 Map + Mutex”。

对象池:Pool

作用:复用对象,减少内存分配和 GC(垃圾回收)压力,提升性能。

原理:内部是一个 “池子”,Get() 从池子里取对象,Put() 把用完的对象放回池子;如果池子空了,会调用 New 函数创建新对象。

package main

import (
	"fmt"
	"sync"
)

// 模拟一个需要频繁创建的对象(比如缓冲区)
type Buffer struct {
	data []byte
}

func main() {
	// 1. 定义对象池
	pool := &sync.Pool{
		New: func() interface{} {
			fmt.Println("创建新的 Buffer 对象")
			return &Buffer{data: make([]byte, 1024)}
		},
	}

	// 2. 第一次获取:池子空了,调用 New 创建
	buf1 := pool.Get().(*Buffer)

	// 判断 buf1 是否为空
	if buf1 == nil || buf1.data == nil {
		fmt.Println("buf1 是空对象")
	} else {
		fmt.Println("buf1 是有效对象")
	}
	// buf1 是有效对象

	// 3. 用完放回池子
	pool.Put(buf1)
	fmt.Println("buf1 放回池子")

	// 4. 第二次获取:直接从池子取,不创建新对象
	buf2 := pool.Get().(*Buffer)

	// 判断 buf2 是否与 buf1 是同一个对象
	if buf1 == buf2 {
		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():通知一个等待的 goroutine;
  • Broadcast():通知所有等待的 goroutine;
  • 必须和 Mutex 一起用
// 生产者 - 消费者模式
package main

import (
	"fmt"
	"sync"
	"time"
)

func main() {
	var mu sync.Mutex
	cond := sync.NewCond(&mu) // 用 Mutex 创建 Cond
	var queue []int // 共享队列

	// 生产者:往队列里放数据
	producer := func() {
		for i := 1; i <= 3; i++ {
			mu.Lock()
			queue = append(queue, i)
			fmt.Printf("生产了 %d,队列长度:%d\n", i, len(queue))
			cond.Signal() // 通知一个消费者
			mu.Unlock()
			time.Sleep(500 * time.Millisecond)
		}
	}

	// 消费者:从队列里取数据
	consumer := func(id int) {
		for {
			mu.Lock()
			// 队列空了就等待
			for len(queue) == 0 {
				cond.Wait() // 释放锁并阻塞,被通知后重新加锁
			}
			// 取数据
			item := queue[0]
			queue = queue[1:]
			fmt.Printf("消费者 %d 消费了 %d,队列长度:%d\n", id, item, len(queue))
			mu.Unlock()
			time.Sleep(1 * time.Second)
		}
	}

	// 启动 1 个生产者,2 个消费者
	go producer()
	go consumer(1)
	go consumer(2)

	// 简单等待
	time.Sleep(5 * time.Second)
}

适用场景:

  • 复杂的生产者 - 消费者模式(需要灵活控制通知逻辑);
  • 需要 “一次性通知所有等待 goroutine” 的场景(如配置更新后通知所有 worker 重新加载);
  • Cond 比较复杂,能用 channel 解决的场景优先用 channel,channel 解决不了再用 Cond

总结

API核心作用一句话适用场景
WaitGroup等待一组 goroutine 完成并发批量处理任务,等所有任务干完再继续
Mutex互斥锁,独占访问共享资源多个 goroutine 同时修改同一个变量
RWMutex读写锁,读并发、写独占读多写少的共享资源(如配置、缓存)
Once保证函数只执行一次单例模式、全局初始化
Map并发安全的 Map多个 goroutine 同时读写的缓存
Pool对象池,复用对象频繁创建销毁的大对象,减少 GC
Cond条件变量,协调等待 - 通知复杂的生产者 - 消费者、需要广播通知的场景
选择原则
  1. 优先用简单的WaitGroup > Mutex > RWMutex > Map > Pool > Cond
  2. 读多写少:优先考虑 RWMutex(锁)或 Map(并发 Map);
  3. 性能优化:频繁创建大对象时用 Pool
  4. 复杂协调:channel 解决不了的场景再用 Cond