在Go语言的并发编程中,多个goroutine同时修改共享变量时,数据竞争是常见的“坑”。比如统计API请求量的计数器,如果没有同步机制,最终结果可能远小于实际请求数。如何实现一个线程安全的计数器?本文从原理到代码,讲解三种主流方案,并对比它们的适用场景。

一、问题根源:无同步的计数器会怎样?
先看一段错误代码:
type Counter struct {
count int
}
func (c *Counter) Inc() {
c.count++ // 非原子操作:读→加1→写,可能被打断
}
func (c *Counter) Get() int {
return c.count
}
当100个goroutine各调用100次Inc(),理想结果是10000,但实际可能只有几千。用Go的race detector(go run -race main.go)运行,会检测到数据竞争:多个goroutine同时访问共享变量c.count,未同步。
二、方案1:互斥锁(sync.Mutex)——通用安全方案
sync.Mutex是Go标准库提供的互斥锁,实现“同一时间只有一个goroutine进入临界区”的效果。临界区即修改共享变量的代码块。
实现代码
import "sync"
type MutexCounter struct {
count int
mu sync.Mutex // 互斥锁
}
// Inc 原子递增:加锁确保独占访问
func (c *MutexCounter) Inc() {
c.mu.Lock() // 加锁,阻塞其他goroutine
defer c.mu.Unlock() // 确保解锁(即使panic也会执行)
c.count++
}
// Get 安全读取:同样需要加锁
func (c *MutexCounter) Get() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.count
}
核心原理
Lock():阻塞后续调用者,直到当前持有者执行Unlock()defer Unlock():避免忘记解锁导致死锁,是Go中操作锁的最佳实践
测试验证
100个goroutine各调用100次Inc(),最终Get()返回10000,无数据竞争。
三、方案2:读写锁(sync.RWMutex)——读多写少场景优化
如果计数器的读操作远多于写操作(比如统计在线用户数,每秒读100次、写1次),Mutex的性能会受影响——每次读都要独占锁。此时用sync.RWMutex更高效:它支持读锁共享、写锁独占。
实现代码
type RWCounter struct {
count int
rw sync.RWMutex // 读写锁
}
// Inc 写操作:加写锁(独占)
func (c *RWCounter) Inc() {
c.rw.Lock()
defer c.rw.Unlock()
c.count++
}
// Get 读操作:加读锁(共享)
func (c *RWCounter) Get() int {
c.rw.RLock()
defer c.rw.RUnlock()
return c.count
}
核心原理
RLock():多个goroutine可同时持有读锁,仅当有写锁时阻塞Lock():写锁独占,持有写锁时所有读/写锁都会阻塞
优势
读多写少场景下,吞吐量比Mutex提升数倍。比如100个读goroutine和1个写goroutine,Mutex串行处理,RWMutex可并行处理读请求。
四、方案3:原子操作(sync/atomic)——无锁高性能方案
对于简单数值操作(int32/int64的加减、加载、存储),Go标准库的sync/atomic包提供了CPU级别的原子指令,无需锁即可保证线程安全,性能最优。
实现代码
import "sync/atomic"
type AtomicCounter struct {
count int32 // 必须用atomic支持的类型(int32/int64等)
}
// Inc 原子递增:AddInt32返回新值(可选)
func (c *AtomicCounter) Inc() {
atomic.AddInt32(&c.count, 1)
}
// Get 原子读取
func (c *AtomicCounter) Get() int32 {
return atomic.LoadInt32(&c.count)
}
注意事项
- 仅支持特定类型:int32、int64、uint32、uint64、uintptr、pointer
- 仅支持简单操作:加减、加载、存储(无法处理复杂逻辑,比如
count = count*2 +1) - 函数参数必须是指针,且不能为nil
五、三种方案对比与选择
| 方案 | 适用场景 | 性能 | 复杂度 | 限制 |
|---|---|---|---|---|
| Mutex | 读写比例均衡、复杂操作 | 一般 | 低 | 无(通用) |
| RWMutex | 读多写少、简单操作 | 较好(读多) | 中 | 读锁不能升级为写锁 |
| Atomic | 简单数值操作(加减等) | 最优 | 低 | 仅支持特定类型/操作 |
实际选择举例
- 电商订单数统计(读多写少):选
RWMutex - API请求量统计(写多、简单加减):选
Atomic - 带复杂逻辑的计数器(比如inc后更新其他字段):选
Mutex
六、总结
线程安全计数器的核心是同步对共享变量的访问,Go提供了三种成熟方案:
Mutex:通用,适合大多数场景RWMutex:读多写少场景的性能优化Atomic:无锁,适合简单数值操作
没有“最好”的方案,只有“最适合”的方案。实际开发中,需结合读写比例、操作复杂度选择,必要时用race detector检测数据竞争,用benchmark测试性能。
关键提醒:永远不要假设“简单操作就是原子的”,Go中只有sync/atomic包的操作是原子的,其他都需要同步机制。

发表评论