前言
Go的goroutine和channel解决了大部分并发问题,但有些场景下,sync包提供的原语更简洁高效。比如保护共享变量、等待一组goroutine完成、确保初始化只执行一次等。
本文整理sync包中常用类型的使用方法和注意事项,配合实际代码示例。
1. Mutex:互斥锁
最基础的锁,同一时刻只有一个goroutine能持有。
1.1 基本用法
package main
import (
"fmt"
"sync"
)
type Counter struct {
mu sync.Mutex
value int
}
func (c *Counter) Inc() {
c.mu.Lock()
defer c.mu.Unlock()
c.value++
}
func (c *Counter) Value() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.value
}
func main() {
var wg sync.WaitGroup
counter := &Counter{}
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
counter.Inc()
}()
}
wg.Wait()
fmt.Println("Final value:", counter.Value()) // 1000
}
1.2 常见错误
错误1:忘记Unlock
func (c *Counter) Inc() {
c.mu.Lock()
c.value++
// 忘记 Unlock,其他goroutine会永远阻塞
}
用defer可以避免这个问题,即使函数panic也能正常解锁。
错误2:复制带锁的结构体
func main() {
c1 := Counter{}
c2 := c1 // 错误:复制了mutex
// c1和c2共享同一把锁的状态,行为不可预期
}
解决方案:传递指针,或者使用noCopy模式。
type Counter struct {
mu sync.Mutex
value int
_ noCopy // go vet会检查复制
}
type noCopy struct{}
func (*noCopy) Lock() {}
func (*noCopy) Unlock() {}
错误3:重复Lock(死锁)
func (c *Counter) Double() {
c.mu.Lock()
defer c.mu.Unlock()
c.Inc() // Inc里面也会Lock,死锁
}
解决方案:拆分内部方法,不加锁的版本供内部调用。
func (c *Counter) inc() {
c.value++ // 不加锁,仅内部使用
}
func (c *Counter) Inc() {
c.mu.Lock()
defer c.mu.Unlock()
c.inc()
}
func (c *Counter) Double() {
c.mu.Lock()
defer c.mu.Unlock()
c.inc()
c.inc()
}
2. RWMutex:读写锁
读多写少的场景下,用读写锁比互斥锁性能好。
- 多个goroutine可以同时持有读锁
- 写锁是排他的,持有写锁时不能有其他读锁或写锁
2.1 基本用法
type Config struct {
mu sync.RWMutex
data map[string]string
}
func (c *Config) Get(key string) string {
c.mu.RLock()
defer c.mu.RUnlock()
return c.data[key]
}
func (c *Config) Set(key, value string) {
c.mu.Lock()
defer c.mu.Unlock()
c.data[key] = value
}
func (c *Config) GetAll() map[string]string {
c.mu.RLock()
defer c.mu.RUnlock()
// 返回副本,防止外部修改
result := make(map[string]string, len(c.data))
for k, v := range c.data {
result[k] = v
}
return result
}
2.2 性能对比
func BenchmarkMutex(b *testing.B) {
var mu sync.Mutex
var value int
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
mu.Lock()
_ = value
mu.Unlock()
}
})
}
func BenchmarkRWMutex(b *testing.B) {
var mu sync.RWMutex
var value int
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
mu.RLock()
_ = value
mu.RUnlock()
}
})
}
读多写少时,RWMutex明显更快;但如果写操作频繁,RWMutex开销反而更大。
2.3 注意事项
- 读锁内不要调用写锁方法(会死锁)
- RWMutex是写优先的,有goroutine等待写锁时,后续读锁请求会阻塞
- 不要在热点路径滥用RWMutex,如果读写比例接近,用Mutex更简单
3. WaitGroup:等待一组goroutine
3.1 基本用法
func main() {
var wg sync.WaitGroup
urls := []string{
"https://example.com",
"https://example.org",
"https://example.net",
}
for _, url := range urls {
wg.Add(1)
go func(url string) {
defer wg.Done()
resp, err := http.Get(url)
if err != nil {
fmt.Println("Error:", err)
return
}
defer resp.Body.Close()
fmt.Println(url, resp.Status)
}(url)
}
wg.Wait()
fmt.Println("All done")
}
3.2 常见错误
错误1:Add在goroutine内部调用
// 错误
for _, url := range urls {
go func(url string) {
wg.Add(1) // 可能在Wait之后执行
defer wg.Done()
// ...
}(url)
}
wg.Wait() // 可能提前返回
错误2:Done调用次数不匹配
wg.Add(1)
go func() {
if someCondition {
return // 忘记Done
}
wg.Done()
}()
用defer可以确保一定执行。
3.3 带超时的等待
WaitGroup本身不支持超时,可以配合channel实现:
func waitWithTimeout(wg *sync.WaitGroup, timeout time.Duration) bool {
done := make(chan struct{})
go func() {
wg.Wait()
close(done)
}()
select {
case <-done:
return true
case <-time.After(timeout):
return false
}
}
func main() {
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
time.Sleep(2 * time.Second)
}()
if waitWithTimeout(&wg, 1*time.Second) {
fmt.Println("Completed")
} else {
fmt.Println("Timeout")
}
}
4. Once:确保只执行一次
4.1 典型场景:单例初始化
type Database struct {
conn *sql.DB
}
var (
dbInstance *Database
dbOnce sync.Once
)
func GetDB() *Database {
dbOnce.Do(func() {
conn, err := sql.Open("mysql", "dsn")
if err != nil {
panic(err)
}
dbInstance = &Database{conn: conn}
})
return dbInstance
}
4.2 注意事项
Once.Do只执行一次,即使panic了也不会重试
var once sync.Once
var config *Config
func GetConfig() *Config {
once.Do(func() {
data, err := ioutil.ReadFile("config.json")
if err != nil {
panic(err) // panic后,once.Do不会再执行
}
json.Unmarshal(data, &config)
})
return config // 如果上面panic了,这里返回nil
}
如果需要重试,不能用sync.Once:
type LazyConfig struct {
mu sync.Mutex
config *Config
}
func (l *LazyConfig) Get() (*Config, error) {
l.mu.Lock()
defer l.mu.Unlock()
if l.config != nil {
return l.config, nil
}
// 可重试的初始化
data, err := ioutil.ReadFile("config.json")
if err != nil {
return nil, err
}
var cfg Config
if err := json.Unmarshal(data, &cfg); err != nil {
return nil, err
}
l.config = &cfg
return l.config, nil
}
5. Cond:条件变量
用于goroutine间的信号通知,比channel更底层。
5.1 生产者消费者模式
type Queue struct {
mu sync.Mutex
cond *sync.Cond
items []int
}
func NewQueue() *Queue {
q := &Queue{}
q.cond = sync.NewCond(&q.mu)
return q
}
func (q *Queue) Put(item int) {
q.mu.Lock()
defer q.mu.Unlock()
q.items = append(q.items, item)
q.cond.Signal() // 通知一个等待的goroutine
}
func (q *Queue) Get() int {
q.mu.Lock()
defer q.mu.Unlock()
for len(q.items) == 0 {
q.cond.Wait() // 释放锁并等待
}
item := q.items[0]
q.items = q.items[1:]
return item
}
5.2 Broadcast:通知所有等待者
type Barrier struct {
mu sync.Mutex
cond *sync.Cond
count int
target int
}
func NewBarrier(n int) *Barrier {
b := &Barrier{target: n}
b.cond = sync.NewCond(&b.mu)
return b
}
func (b *Barrier) Wait() {
b.mu.Lock()
defer b.mu.Unlock()
b.count++
if b.count == b.target {
b.cond.Broadcast() // 所有人到齐,通知全部
return
}
for b.count < b.target {
b.cond.Wait()
}
}
实际项目中,大部分场景用channel就够了,Cond用得比较少。
6. Pool:对象复用池
减少内存分配和GC压力。
6.1 基本用法
var bufferPool = sync.Pool{
New: func() interface{} {
return new(bytes.Buffer)
},
}
func process(data []byte) string {
buf := bufferPool.Get().(*bytes.Buffer)
defer func() {
buf.Reset()
bufferPool.Put(buf)
}()
buf.Write(data)
// 处理...
return buf.String()
}
6.2 实际案例:JSON编码
var jsonEncoderPool = sync.Pool{
New: func() interface{} {
return &bytes.Buffer{}
},
}
func ToJSON(v interface{}) ([]byte, error) {
buf := jsonEncoderPool.Get().(*bytes.Buffer)
defer func() {
buf.Reset()
jsonEncoderPool.Put(buf)
}()
encoder := json.NewEncoder(buf)
if err := encoder.Encode(v); err != nil {
return nil, err
}
// 返回副本,因为buf会被复用
result := make([]byte, buf.Len())
copy(result, buf.Bytes())
return result, nil
}
6.3 注意事项
- Pool中的对象可能随时被回收(GC时),不要存储重要状态
- Get返回的对象可能是复用的,使用前要Reset
- Pool不是缓存,不保证对象一定存在
- 要确保Put回去的对象是干净的
7. Map:并发安全的map
7.1 基本用法
var cache sync.Map
func Get(key string) (interface{}, bool) {
return cache.Load(key)
}
func Set(key string, value interface{}) {
cache.Store(key, value)
}
func GetOrSet(key string, value interface{}) interface{} {
actual, _ := cache.LoadOrStore(key, value)
return actual
}
func Delete(key string) {
cache.Delete(key)
}
func Range() {
cache.Range(func(key, value interface{}) bool {
fmt.Println(key, value)
return true // 返回false停止遍历
})
}
7.2 适用场景
sync.Map针对以下两种场景优化:
- key只写一次但读很多次(缓存)
- 多个goroutine读写不同的key
其他场景下,用普通map+Mutex可能更好。
7.3 性能对比
// sync.Map
func BenchmarkSyncMap(b *testing.B) {
var m sync.Map
b.RunParallel(func(pb *testing.PB) {
i := 0
for pb.Next() {
m.Store(i, i)
m.Load(i)
i++
}
})
}
// map + Mutex
func BenchmarkMapMutex(b *testing.B) {
m := make(map[int]int)
var mu sync.Mutex
b.RunParallel(func(pb *testing.PB) {
i := 0
for pb.Next() {
mu.Lock()
m[i] = i
mu.Unlock()
mu.Lock()
_ = m[i]
mu.Unlock()
i++
}
})
}
读多写少时sync.Map更快,写多时普通map+Mutex更好。
8. 原子操作:sync/atomic
比锁更轻量,适合简单的数值操作。
8.1 基本用法
import "sync/atomic"
type Counter struct {
value int64
}
func (c *Counter) Inc() {
atomic.AddInt64(&c.value, 1)
}
func (c *Counter) Dec() {
atomic.AddInt64(&c.value, -1)
}
func (c *Counter) Value() int64 {
return atomic.LoadInt64(&c.value)
}
func (c *Counter) Reset() {
atomic.StoreInt64(&c.value, 0)
}
8.2 CAS操作
func (c *Counter) CompareAndSwap(old, new int64) bool {
return atomic.CompareAndSwapInt64(&c.value, old, new)
}
// 无锁更新
func (c *Counter) Update(fn func(int64) int64) {
for {
old := atomic.LoadInt64(&c.value)
new := fn(old)
if atomic.CompareAndSwapInt64(&c.value, old, new) {
return
}
}
}
8.3 atomic.Value:存储任意类型
var config atomic.Value
func UpdateConfig(cfg *Config) {
config.Store(cfg)
}
func GetConfig() *Config {
return config.Load().(*Config)
}
注意:atomic.Value存储的类型必须一致,第一次Store什么类型,后续就只能Store相同类型。
总结
| 原语 | 适用场景 | 注意事项 |
|---|---|---|
| Mutex | 保护共享变量 | 用defer确保Unlock,不要复制 |
| RWMutex | 读多写少 | 写优先,读锁内不要写 |
| WaitGroup | 等待一组goroutine | Add在goroutine外调用 |
| Once | 单例初始化 | panic不会重试 |
| Cond | 条件等待 | 大部分场景用channel更好 |
| Pool | 对象复用 | 不是缓存,对象可能被回收 |
| Map | 并发安全map | 只在特定场景有优势 |
| atomic | 简单数值操作 | 比锁更轻量 |
选择建议:
- 能用channel就用channel,更符合Go的设计哲学
- 保护简单变量用Mutex,不要过度优化
- 读多写少考虑RWMutex,但要实测确认有收益
- 单例初始化用Once,简单可靠
- 热点路径的简单计数用atomic,避免锁竞争
这些并发原语各有适用场景,关键是理解其语义和限制,根据实际需求选择。