17370845950

Go语言并发下如何共享数据_Golang数据同步方案
Go并发共享数据必须用同步机制防data race;sync.Mutex保护临界区,注意非重入、锁粒度和defer时机;sync.RWMutex适合读多写少;sync/atomic用于基础类型无锁操作;channel本质是通信而非锁,不宜滥用。

Go 语言并发下共享数据,不能靠“约定”或“自觉”,必须用同步机制;否则 data race 是大概率事件,且可能在压测或上线后才暴露。

sync.Mutex 保护临界区最直接

只要多个 goroutine 会读写同一变量(比如一个 map 或结构体字段),就必须加锁。注意:只读一般不需锁,但若读操作与写操作并发,且该读发生在写中间(如遍历 map 同时有 delete),仍可能 panic。

  • sync.Mutex 是非重入锁,同一个 goroutine 重复 Lock() 会死锁
  • 习惯用 defer mu.Unlock(),但要确保 mu.Lock() 已执行,否则 defer 会 unlock 未 lock 的 mutex
  • 锁粒度宁小勿大:不要整个函数都包在 mu.Lock() / mu.Unlock() 里,只包真正访问共享数据的几行
var mu sync.Mutex
var counter int

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

sync.RWMutex 适合读多写少场景

当共享数据被频繁读取、极少修改(如配置缓存、白名单列表),sync.RWMutex 能显著提升并发吞吐。多个 goroutine 可同时持有读锁,但写锁会阻塞所有读写。

  • RUnlock() 必须和 RLock() 配对,漏掉会导致后续写锁永远无法获取
  • 不能在持有读锁时升级为写锁(即先 Rlock 再想 Lock)——这会死锁,必须先 RUnlock()Lock()
  • 写操作期间,新来的读请求会排队,直到写完成;这点和读写锁语义一致,但容易误以为“读可以插队”
var rwmu sync.RWMutex
var config map[string]string

func getConfig(key string) string {
    rwmu.RLock()
    defer rwmu.RUnlock()
    return config[key]
}

func updateConfig(k, v string) {
    rwmu.Lock()
    defer rwmu.Unlock()
    config[k] = v
}

sync/atomic 处理简单整数或指针的无锁更新

仅适用于基础类型:int32int64uint32uint64uintptr*T。不是所有字段都能原子化,比如 struct 字段或 int(在 32 位系统上非原子)。

  • atomic.LoadInt64(&x)atomic.StoreInt64(&x, v) 是最常用组合
  • 避免混用:不要一部分代码用 atomic,另一部分直接读写变量,那等于没保护
  • atomic.AddInt64 返回新值,适合计数器累加后判断阈值
var hits int64

func recordHit() {
    atomic.AddInt64(&hits, 1)
}

func getHits() int64 {
    return atomic.LoadInt64(&hits)
}

channel 不是万能同步工具,别硬套

channel 本质是通信机制,不是锁。它适合“传递所有权”或“协调生命周期”,比如生产者-消费者、信号通知、限流。但若只为保护一个计数器而开 channel,性能差、逻辑绕、还容易漏 close 或 goroutine 泄漏。

  • 用 channel 做同步,通常意味着你把“状态变更”转成了“消息发送”,适合跨 goroutine 协作,不适合高频、细粒度的数据保护
  • 带缓冲 channel(如 make(chan struct{}, 1))模拟互斥锁是可行的,但可读性差、调试难,远不如 sync.Mutex 直观
  • 如果发现要用 select + default 非阻塞尝试获取 channel,基本说明设计已偏离 Go 的 channel 原意

真正难的不是选哪个同步原语,而是识别出哪些变量是共享的、哪些访问路径是并发的——这需要结合调用栈和 goroutine 生命周期去

分析,而不是看到 go f() 就给所有变量加锁。