Skip to content
奥运的 Blog
Go back

Go sync 包详解:Mutex、RWMutex 与 WaitGroup

当多个 goroutine 需要访问共享数据时,必须使用同步原语保证数据安全。Go 的 sync 包提供了多种同步工具,本文重点讲解 MutexRWMutexWaitGroup 以及 sync.Once

Mutex:互斥锁

package main

import (
    "fmt"
    "sync"
)

type SafeCounter struct {
    mu    sync.Mutex
    count int
}

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

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

func main() {
    counter := &SafeCounter{}
    var wg sync.WaitGroup

    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            counter.Inc()
        }()
    }

    wg.Wait()
    fmt.Println("Final count:", counter.Value()) // 1000
}

注意sync.Mutex 是不可重入的,同一个 goroutine 重复加锁会死锁:

func (c *SafeCounter) bad() {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.Inc() // deadlock! Inc() 也会加锁
}

RWMutex:读写锁

读多写少的场景用 RWMutex 性能更好:

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

func NewCache() *Cache {
    return &Cache{data: make(map[string]string)}
}

// 写操作:排他锁
func (c *Cache) Set(key, value string) {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.data[key] = value
}

// 读操作:共享锁,多个 goroutine 可以同时读
func (c *Cache) Get(key string) (string, bool) {
    c.mu.RLock()
    defer c.mu.RUnlock()
    val, ok := c.data[key]
    return val, ok
}

func (c *Cache) Delete(key string) {
    c.mu.Lock()
    defer c.mu.Unlock()
    delete(c.data, key)
}

RWMutex 的规则:

WaitGroup:等待多个 goroutine 完成

func processItems(items []string) []string {
    var (
        wg      sync.WaitGroup
        mu      sync.Mutex
        results []string
    )

    for _, item := range items {
        wg.Add(1)
        go func(s string) {
            defer wg.Done()

            result := process(s) // 耗时处理

            mu.Lock()
            results = append(results, result)
            mu.Unlock()
        }(item)
    }

    wg.Wait()
    return results
}

常见错误wg.Add() 必须在 goroutine 启动前调用:

// 错误:可能在 Wait() 之前没有 Add
for _, item := range items {
    go func(s string) {
        wg.Add(1)    // 错误!可能在 Wait() 之后才调用
        defer wg.Done()
        process(s)
    }(item)
}

// 正确
for _, item := range items {
    wg.Add(1)        // 在 goroutine 启动前调用
    go func(s string) {
        defer wg.Done()
        process(s)
    }(item)
}

sync.Once:只执行一次

常用于单例模式或延迟初始化:

type DB struct {
    conn *sql.DB
}

var (
    dbInstance *DB
    once       sync.Once
)

func GetDB() *DB {
    once.Do(func() {
        conn, err := sql.Open("mysql", dsn)
        if err != nil {
            panic(err)
        }
        dbInstance = &DB{conn: conn}
    })
    return dbInstance
}

sync.Once 保证 Do 中的函数只执行一次,即使多个 goroutine 同时调用。

sync.Map:并发安全的 Map

适用于读多写少,或 key 集合相对固定的场景:

var m sync.Map

// 存储
m.Store("key", "value")

// 读取
val, ok := m.Load("key")

// 存在则读取,不存在则存储(原子操作)
actual, loaded := m.LoadOrStore("key", "new_value")

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

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

注意sync.Map 不适合所有场景,对于写多读少的场景,带 sync.RWMutex 的普通 map 性能更好。

sync.Pool:对象池

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

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

func processRequest(data []byte) string {
    buf := bufPool.Get().(*bytes.Buffer)
    defer func() {
        buf.Reset()
        bufPool.Put(buf)
    }()

    buf.Write(data)
    // 处理数据...
    return buf.String()
}

sync.Pool 适合:

不适合:持久化的对象(Pool 中的对象可能随时被 GC 回收)。

atomic 包:原子操作

对于简单的数值操作,atomicMutex 性能更好:

import "sync/atomic"

type AtomicCounter struct {
    count int64
}

func (c *AtomicCounter) Inc() {
    atomic.AddInt64(&c.count, 1)
}

func (c *AtomicCounter) Value() int64 {
    return atomic.LoadInt64(&c.count)
}

// CAS(Compare And Swap):乐观锁的基础
func (c *AtomicCounter) CompareAndSwap(old, new int64) bool {
    return atomic.CompareAndSwapInt64(&c.count, old, new)
}

死锁避免

// 常见死锁场景:加锁顺序不一致
var mu1, mu2 sync.Mutex

// goroutine 1
mu1.Lock()
mu2.Lock() // 如果 goroutine2 已持有 mu2 并等待 mu1,死锁!

// goroutine 2
mu2.Lock()
mu1.Lock()

// 解决:保证加锁顺序一致
// 始终先加 mu1,再加 mu2

使用 go build -race 检测数据竞争:

go build -race ./...
go test -race ./...

总结

工具适用场景
sync.Mutex保护共享数据,读写都需要锁
sync.RWMutex读多写少的共享数据
sync.WaitGroup等待多个 goroutine 完成
sync.Once只执行一次的初始化
sync.Map并发安全的 map,key 相对稳定
sync.Pool复用临时对象,减少 GC
atomic单个数值的原子操作

核心原则:能用 channel 通信的,优先用 channel;必须共享内存时,用最细粒度的锁


Share this post on:

Previous Post
Docker 容器化 Go 应用实践
Next Post
MySQL 索引优化实战笔记