第 11 章:sync 包与并发安全

jerry北京市2026年5月9日Go 20 次阅读 约 12 分钟
第 11 章:sync 包与并发安全

掌握互斥锁、读写锁、WaitGroup、Once、sync.Map 和原子操作,编写并发安全的代码。


11.1 数据竞争

多个 goroutine 同时读写共享变量会导致数据竞争:

// 有数据竞争的代码
counter := 0
var wg sync.WaitGroup

for i := 0; i < 1000; i++ {
    wg.Add(1)
    go func() {
        defer wg.Done()
        counter++  // 数据竞争!
    }()
}

wg.Wait()
fmt.Println(counter)  // 结果不确定,可能小于 1000

检测数据竞争:

go run -race main.go
go test -race ./...

11.2 Mutex(互斥锁)

var (
    counter int
    mu      sync.Mutex
)

func increment() {
    mu.Lock()
    defer mu.Unlock()
    counter++
}

// 使用
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
    wg.Add(1)
    go func() {
        defer wg.Done()
        increment()
    }()
}
wg.Wait()
fmt.Println(counter)  // 1000(确定的结果)

封装到结构体中

type SafeCounter struct {
    mu    sync.Mutex
    count int
}

func (c *SafeCounter) Increment() {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.count++
}

func (c *SafeCounter) Value() int {
    c.mu.Lock()
    defer c.mu.Unlock()
    return c.count
}

11.3 RWMutex(读写锁)

读多写少的场景,RWMutex 比 Mutex 性能更好:

type SafeMap struct {
    mu   sync.RWMutex
    data map[string]string
}

func (m *SafeMap) Get(key string) (string, bool) {
    m.mu.RLock()          // 读锁:多个读可以并发
    defer m.mu.RUnlock()
    v, ok := m.data[key]
    return v, ok
}

func (m *SafeMap) Set(key, value string) {
    m.mu.Lock()           // 写锁:独占
    defer m.mu.Unlock()
    m.data[key] = value
}
操作 Mutex RWMutex
读-读 互斥 并发
读-写 互斥 互斥
写-写 互斥 互斥

11.4 WaitGroup

等待一组 goroutine 完成:

var wg sync.WaitGroup

for i := 0; i < 5; i++ {
    wg.Add(1)
    go func(id int) {
        defer wg.Done()
        fmt.Printf("任务 %d 完成\n", id)
    }(i)
}

wg.Wait()
fmt.Println("全部完成")

注意事项:

  • Add 必须在 goroutine 启动前调用
  • Done 等价于 Add(-1)
  • Wait 阻塞直到计数器归零
  • 计数器不能为负数(会 panic)

11.5 Once

确保某个操作只执行一次(典型场景:单例初始化):

var (
    instance *Database
    once     sync.Once
)

func GetDB() *Database {
    once.Do(func() {
        fmt.Println("初始化数据库连接...")
        instance = &Database{
            // 初始化配置
        }
    })
    return instance
}

// 无论调用多少次,初始化只执行一次
db1 := GetDB()
db2 := GetDB()  // 直接返回已初始化的实例

11.6 sync.Map

并发安全的 map,适用于读多写少的场景:

var m sync.Map

// 存储
m.Store("key1", "value1")
m.Store("key2", 42)

// 读取
v, ok := m.Load("key1")
if ok {
    fmt.Println(v)  // value1
}

// 读取或存储(如果不存在则存储)
actual, loaded := m.LoadOrStore("key3", "default")
fmt.Println(actual, loaded)  // default, false

// 删除
m.Delete("key1")

// 遍历
m.Range(func(key, value interface{}) bool {
    fmt.Printf("%v: %v\n", key, value)
    return true  // 返回 false 停止遍历
})

sync.Map vs 加锁的 map:

  • sync.Map 适合:读多写少、key 相对稳定
  • 加锁 map 适合:读写均衡、需要批量操作

11.7 原子操作(atomic)

比锁更轻量的并发安全操作:

import "sync/atomic"

var counter int64

// 原子加
atomic.AddInt64(&counter, 1)

// 原子读
v := atomic.LoadInt64(&counter)

// 原子写
atomic.StoreInt64(&counter, 100)

// CAS(Compare And Swap)
swapped := atomic.CompareAndSwapInt64(&counter, 100, 200)
// 如果 counter == 100,则设为 200,返回 true

Go 1.19+ atomic.Int64

var counter atomic.Int64

counter.Add(1)
counter.Store(100)
v := counter.Load()
swapped := counter.CompareAndSwap(100, 200)

原子操作 vs 锁:

  • 原子操作:适合简单的数值操作,性能更好
  • 锁:适合复杂的临界区操作

11.8 sync.Cond

条件变量,用于 goroutine 之间的条件等待:

var (
    mu    sync.Mutex
    cond  = sync.NewCond(&mu)
    ready bool
)

// 等待方
go func() {
    cond.L.Lock()
    for !ready {
        cond.Wait()  // 释放锁并等待通知
    }
    fmt.Println("收到通知,开始工作")
    cond.L.Unlock()
}()

// 通知方
time.Sleep(time.Second)
cond.L.Lock()
ready = true
cond.Signal()  // 唤醒一个等待者(Broadcast 唤醒所有)
cond.L.Unlock()

11.9 sync.Pool

对象池,用于复用临时对象,减少 GC 压力:

var bufferPool = sync.Pool{
    New: func() interface{} {
        return new(bytes.Buffer)
    },
}

func processRequest() {
    buf := bufferPool.Get().(*bytes.Buffer)
    defer func() {
        buf.Reset()
        bufferPool.Put(buf)
    }()

    buf.WriteString("处理请求...")
    fmt.Println(buf.String())
}

注意:Pool 中的对象可能在任何时候被 GC 回收,不要存储需要持久化的数据。

11.10 面试要点

  1. Mutex 和 RWMutex 的区别?

    • Mutex:读写都互斥
    • RWMutex:读读并发,读写互斥,写写互斥
  2. sync.Map 的适用场景?

    • 读多写少,key 相对稳定
    • 不适合频繁写入的场景
  3. 原子操作和锁的区别?

    • 原子操作更轻量,适合简单数值操作
    • 锁适合复杂临界区
  4. sync.Pool 的作用?

    • 复用临时对象,减少内存分配和 GC 压力
    • 对象可能被 GC 回收,不保证持久性
  5. 如何检测数据竞争?

    • go run -racego test -race

练习

  1. 使用 Mutex 实现一个并发安全的银行账户(存款、取款、查询余额)
  2. 使用 RWMutex 实现一个并发安全的缓存
  3. 使用 sync.Once 实现单例模式
  4. 使用 atomic 实现一个无锁计数器,与 Mutex 版本做性能对比

← 上一章:Channel 与并发模式 | 下一章:反射与泛型 →

评论

登录 后发表评论

暂无评论