Go atomic 入门:什么时候需要原子计数,什么时候该用锁

本文讲解 Go sync/atomic 的基础用法,包括原子计数、并发读取、atomic 类型和与 mutex 的取舍,帮助初学者安全处理共享状态。

并发计数不是 count++ 那么简单

多个 goroutine 同时修改一个变量时,普通 count++ 不是安全操作。它看起来是一行,实际包含读取、加一、写回多个步骤。两个 goroutine 交错执行时,更新可能丢失。Go 的 race detector 也会报告数据竞争。

解决共享状态有多种方式:用 channel 汇总,用 sync.Mutex 加锁,用 sync/atomic 做原子操作。atomic 适合非常简单的共享数值或标志位,比如请求计数、开关状态、轻量指标。它不适合表达复杂业务不变量。初学者要先学会判断边界:能用锁写清楚的,别为了“高性能”硬上 atomic。

这篇文章用请求计数做例子,讲原子操作的入门用法。

一个错误的计数器

type Counter struct {
	value int64
}

func (c *Counter) Inc() {
	c.value++
}

func (c *Counter) Value() int64 {
	return c.value
}

并发调用 Inc 会产生数据竞争。运行:

go test -race ./...

如果测试覆盖到并发路径,race detector 会提醒你。

使用 atomic.Int64

现代 Go 提供了更易用的 atomic 类型:

type Counter struct {
	value atomic.Int64
}

func (c *Counter) Inc() {
	c.value.Add(1)
}

func (c *Counter) Value() int64 {
	return c.value.Load()
}

使用:

var counter Counter

var wg sync.WaitGroup
for i := 0; i < 100; i++ {
	wg.Add(1)
	go func() {
		defer wg.Done()
		counter.Inc()
	}()
}
wg.Wait()

fmt.Println(counter.Value())

AddLoad 都是原子操作。多个 goroutine 同时调用不会产生普通数据竞争。

用在 HTTP 指标里

type Metrics struct {
	requests atomic.Int64
}

func (m *Metrics) Middleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		m.requests.Add(1)
		next.ServeHTTP(w, r)
	})
}

func (m *Metrics) Handler(w http.ResponseWriter, r *http.Request) {
	value := m.requests.Load()
	w.Header().Set("Content-Type", "text/plain; charset=utf-8")
	fmt.Fprintf(w, "requests %d\n", value)
}

注册:

metrics := &Metrics{}
mux.Handle("/api", metrics.Middleware(apiHandler))
mux.HandleFunc("/metrics", metrics.Handler)

这是 atomic 适合的场景:一个独立计数值,写入和读取都很简单。

什么时候用 Mutex 更好

如果状态不止一个字段,并且它们之间有关系,用锁更清楚:

type Account struct {
	mu      sync.Mutex
	balance int64
	updated time.Time
}

func (a *Account) Deposit(cents int64) {
	a.mu.Lock()
	defer a.mu.Unlock()

	a.balance += cents
	a.updated = time.Now()
}

如果你用多个 atomic 字段分别更新,很可能在某个瞬间读到不一致状态。锁能保护一组字段的不变量。

经验很简单:单个计数器、布尔开关、独立指标可以考虑 atomic;多个字段组合、业务规则、复杂状态,优先用 mutex 或 channel。

不要复制 atomic 值

包含 atomic 字段的结构体不应该在使用后被复制:

counter := Counter{}
counter.Inc()

copyCounter := counter // 不要这样做

复制会让状态语义变得混乱。通常把这类对象通过指针传递:

func NewCounter() *Counter {
	return &Counter{}
}

这和包含 sync.Mutex 的结构体类似,使用后都不应该复制。

用 atomic.Bool 做开关

除了计数,原子布尔值也很常见。比如服务正在关闭时,拒绝新任务:

type Worker struct {
	closing atomic.Bool
}

func (w *Worker) Close() {
	w.closing.Store(true)
}

func (w *Worker) Submit(job Job) error {
	if w.closing.Load() {
		return fmt.Errorf("worker is closing")
	}
	// enqueue job
	return nil
}

这个场景里状态很简单:开或关。用 atomic 比加一把锁更轻,也很清楚。

但如果关闭时还要同时修改队列、等待任务、更新多个字段,就应该回到 mutex 或 channel。atomic 只适合独立状态,不适合复杂生命周期管理。越是并发代码,越不要为了少写几行锁而牺牲可读性。

小结

sync/atomic 适合处理简单共享状态,比如原子计数、开关和轻量指标。atomic.Int64 这类类型比旧式函数更易读。并发代码里普通 count++ 不安全,应该用 atomic、mutex、channel 等同步方式。

不要把 atomic 当成万能性能工具。状态一复杂,锁往往更清楚、更安全。并发代码的第一目标是正确,其次才是性能。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页